C.W.K.
Stream
Lesson 02 of 06 · published

Status Code Dispatch — 제 값 하는 specific code

~11 min · semantics, status-codes, dispatch, refinement

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Family 숫자가 80% 데려가. 남은 20% — 201 vs 204, 301 vs 308, 401 vs 403, 400 vs 422 의 차이 — 가 견딜만한 API 와 존중받는 API 를 가르는 거야."

2xx — 성공에 다섯 shape

모든 2xx code 가 request 성공 의미. 특정 code 가 client 한테 어떻게 알려줘:

  • 200 OK — 일반 성공. Response body 가 결과 담음. GET, 새 state 돌려주는 idempotent update, 대부분 일상 성공에 써.
  • 201 Created — Request 가 새 resource 생성. Response 는 새 resource URL 가리키는 Location header 필수 포함. 자주 body 도 새 resource 운반. POST 가 create 와 PUT 이 create (resource 없었을 때) 에 써.
  • 202 Accepted — Request 가 처리 위해 받아짐, 아직 안 끝남. Body 나 Location header 가 client 가 poll 할 수 있는 status URL 가리켜야. Long-running async operation 에 — Stripe payout, video transcoding, council finalization.
  • 204 No Content — 성공, body 명시적 없음. DELETE 후, update 된 resource 안 돌려주는 PUT/PATCH 후 흔함. 항상 response.json() 호출하는 client 는 204 에 죽음.
  • 206 Partial Content — Range request 에 response. Video streaming, 재개 가능 download, 큰 파일 API 가 씀. Body 가 resource 의 byte range 지 전체 아님.

3xx — Redirect 가 method 보존 여부로 갈림

3xx redirect 쌍이 버그 한 class 의 원천: POST 가 redirect 받고 redirected request 가 GET 됨 (301/302 가 역사적으로 허용해서). 해결책: method-preserving code 써.

  • 301 Moved Permanently — "이 URL 영원히 없음; Location 의 거 써." Client 가 북마크 update. 역사적으로 follow 에 method-change 허용 (POST → GET); 현대 가이드는 하지 마.
  • 308 Permanent Redirect — 301 같은데 명시적으로 method 보존. POST 는 POST 유지. Non-GET endpoint 의 영구 이동에 써.
  • 302 Found — 임시 redirect. 역사적으로 method-change 허용.
  • 307 Temporary Redirect — 302 같은데 명시적으로 method 보존. POST 는 POST 유지. Non-GET endpoint 의 임시 redirect 에 써.
  • 304 Not Modified — Cached 사본 여전히 유효; body 안 돌려줌. Conditional-request response (Track 2 lesson 4).

4xx — 헷갈리는 쌍 명예의 전당

경력 비용 치게 하는 쌍 넷:

401 Unauthorized vs 403 Forbidden. 401 = "누구야?" — 자격 증명 보내. 403 = "네가 누군지 알고, 안 돼." 자격 증명 보내도 소용없음. 인증 안 된 사용자한테 403 돌려주면 resource 존재 누설; 인증 성공했는데 401 돌려주는 건 그냥 거짓말. WWW-Authenticate header 가 401 동반해서 client 한테 어떻게 인증할지 알려줘야.

400 Bad Request vs 422 Unprocessable Entity. 400 = "request parse 못 했어." 잘못된 JSON, 누락된 필수 header, malformed URL. 422 = "잘 parse 했는데 내용이 비즈니스 규칙 위반." "not-an-email" 인 email 필드, 음수 quantity. FastAPI/Pydantic 이 맞게 함; 손수 짠 API 다수가 둘을 400 으로 뭉뚱그려 진단 신호 잃음.

404 Not Found vs 410 Gone. 404 = "이게 존재했었는지 미래에 존재할지 몰라." 410 = "이게 있었고, 지워졌고, 돌아오지 않음 — 확인 그만해." 410 은 client 가 index prune 하게 해줌; 404 는 그 신호 안 운반.

409 Conflict. Resource 의 현재 state 때문에 request 적용 불가. 가장 흔한 경우: 오래된 ETag 의 PUT (ETag 있을 땐 412 Precondition Failed 써), 활성 dependent 있는 resource 의 DELETE, uniqueness 제약 위반 POST.

Specific code 는 refinement 지 대체 아냐. Family 숫자에 대해 쓴 client 는 ship 된 모든 HTTP response 의 100% 처리 — 아직 존재 안 하는 code 포함. 특정 code refinement 가 지능 더해 (429 에 retry-with-delay, 401 에 refresh-auth, 201 에 follow-location). 근데 순서 절대 뒤집지 마: family 먼저, code 그 다음.

5xx — 다 retry 의미, 근데 다름

  • 500 Internal Server Error — Server 가 예상 못 한 예외 잡음. Backoff 로 retry; 다음 deploy 에 해결될 수도.
  • 502 Bad Gateway — Server 가 gateway/proxy 인데 upstream 이 malformed response 줌. Retry; upstream 이 잠시 안 좋음.
  • 503 Service Unavailable — Server 가 과부하 혹은 점검 중. Delay 가진 Retry-After 포함해야. 존중해.
  • 504 Gateway Timeout — Server 가 gateway 이고 upstream 이 시간 안에 응답 안 함. Retry; upstream 이 그냥 느렸을 수도.

cwkPippa 의 status code 현실

cwkPippa 는 새 대화에 Location 가진 201, async council finalization 에 202 (client 가 완료 위해 poll), archive-folder DELETE 에 204, malformed chat payload 에 400, 누락 auth 에 401, admin 아닌데 admin route 치면 403, Pydantic 필드 validation 에 422, OpenAI/Anthropic upstream 이 rate-limit 하면 Retry-After 가진 429, 잡히지 않은 예외에 500, backend brain (Codex/Gemini) 죽으면 Retry-After 가진 503. 이 모든 결정이 현실 production 순간에서 옴.

Code

맞는 code 발신 (FastAPI)·python
# Server 쪽 — 각 상황에 맞는 code 발신
from fastapi import FastAPI, HTTPException, status, Response
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post('/users', status_code=201)
async def create_user(payload: dict, response: Response):
    user_id = create_in_db(payload)
    response.headers['Location'] = f'/users/{user_id}'
    return {'id': user_id, **payload}  # 201 + Location

@app.post('/videos/transcode', status_code=202)
async def transcode(payload: dict, response: Response):
    job_id = enqueue_job(payload)
    response.headers['Location'] = f'/jobs/{job_id}'
    return {'job_id': job_id, 'status': 'queued'}  # 202 + poll Location

@app.delete('/users/{uid}', status_code=204)
async def delete_user(uid: str):
    delete_from_db(uid)
    return  # 204 — body 없음

@app.put('/users/{uid}', status_code=308)  # method-preserving 영구 redirect
async def relocated(uid: str, response: Response):
    response.headers['Location'] = f'/v2/users/{uid}'  # POST→POST, PUT→PUT
    return None
Production client — family 먼저, code refinement 그 다음·python
# Client 쪽 — family 숫자로 분기, specific code 로 refine
import httpx

def handle(resp: httpx.Response):
    family = resp.status_code // 100
    code = resp.status_code

    if family == 2:
        if code == 201:
            return {'created': True, 'location': resp.headers['Location'], **resp.json()}
        if code == 202:
            return {'async': True, 'poll': resp.headers['Location']}
        if code == 204:
            return None  # .json() 안 함 — body 비어있음
        return resp.json()  # 200, 206 등

    if family == 4:
        if code == 401: refresh_auth_and_retry()
        elif code == 403: raise PermissionError(resp.text)
        elif code == 404: return None  # missing 으로 다룸
        elif code == 409: handle_conflict(resp)
        elif code == 410: forget_url_forever(resp.request.url)
        elif code == 422: raise ValidationError(resp.json())
        elif code == 429:
            wait = int(resp.headers.get('Retry-After', 1))
            return retry_after(wait)
        raise ClientError(code, resp.text)

    if family == 5:
        if code == 503:
            wait = int(resp.headers.get('Retry-After', 5))
            return retry_after(wait)
        return retry_with_backoff()
각 refined code 의 실제 response 가 어떻게 생겼는지·bash
# 각 status code 가 실전에서 어떻게 보이는지
# 201 with Location
curl -i -X POST https://api.example.com/users -d '{"name":"Pippa"}'
# HTTP/1.1 201 Created
# Location: /users/u_abc123
# Content-Type: application/json
# ...

# 204 — body 없음
curl -i -X DELETE https://api.example.com/users/42
# HTTP/1.1 204 No Content
# (빈 body)

# 429 — Retry-After 존중
curl -i https://api.example.com/heavy-endpoint
# HTTP/1.1 429 Too Many Requests
# Retry-After: 60
# Content-Type: application/json
# {"error":"rate_limited","window_seconds":60}

External links

Exercise

Endpoint 다섯, 각각 다른 status code 돌려주는 작은 FastAPI server 만들어: POST /items (201 + Location), POST /jobs (202 + Location), DELETE /items/{id} (204), GET /items/{id} with optional ?force_410=true (404 vs 410), POST /strict-create payload 에 필수 필드 누락이면 422. 각각 curl -i 로 호출하고 response 검사. 그 다음 각각에 올바르게 dispatch 하는 Python client 함수 써 — 절대 안 죽고, refined code 마다 항상 옳은 일.
Hint
FastAPI 가 route decorator 와 handler 중간의 Response.status_code 에 status_code 노출. Location 동적 설정 위해 response: Response injection 써. Client 쪽에선 family 먼저 분기 (family check 가 바깥 if/elif), 그 다음 refine. Dispatch 가 절대 else: raise Exception 안 됨 — 알려지지 않은 code 면 family 기본으로 fallback.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.