C.W.K.
Stream
Lesson 07 of 07 · published

Error Envelope 설계 — Client 한테 작업할 거 줘

~10 min · rest-design, errors, error-envelope, rfc7807

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Status code 가 '뭔가 잘못됨' 말함. Error body 가 client 한테 뭐가 잘못됐는지, 왜, 그에 대해 뭘 할지 알려주는 곳. 구조화된 body 없으면 모든 error 가 똑같이 보이고 support 인박스가 차."

Error body 가 해야 하는 일

잘 설계된 error response 가 자주 한 번에 네 청중 서빙:

  • 최종 사용자, UI error 메시지 보고 뭘 고쳐야 하는지 알아야.
  • 통합하는 개발자, client 코드 debug 중이고 분기할 machine-readable code 필요.
  • Support desk 의 on-call 엔지니어, server 로그와 correlate 위해 request ID 필요.
  • 자동 monitoring, code 별 error 세고 outlier 감지 원함.

Status code 하나가 넷 다 운반 못 함. Body 가 해야.

최소 envelope

필드 셋이 80% 케이스 커버:

{
  "error": {
    "code": "invalid_email_format",
    "message": "이메일 주소 'pippa@@example.com' 가 유효하지 않습니다.",
    "request_id": "req_xyz789"
  }
}

code 가 machine-readable 식별자 — client 가 여기서 분기, 절대 human 메시지에서 안 함. message 가 사람용 (그리고 점점 API response 읽는 AI 에이전트용). request_id 가 client 와 server 로그 사이 loop 닫음.

Multi-error envelope (validation)

Validation 에러는 특별: 한 request 에 문제 여러 (email malformed AND password 너무 짧음 AND age 음수). 하나씩 surface 하면 round trip 셋 강제. 다 돌려줘:

{
  "error": {
    "code": "validation_failed",
    "message": "필드 3개에 대해 request validation 실패.",
    "errors": [
      {"field": "email",    "code": "invalid_format",     "message": "..."},
      {"field": "password", "code": "too_short",          "message": "...", "min": 8},
      {"field": "age",      "code": "out_of_range",       "message": "...", "min": 0}
    ],
    "request_id": "req_xyz789"
  }
}

FastAPI + Pydantic 이 422 response 에 즉시 이거 해. 손수 짠 API 다수가 이거 underspecify 하고 client 가 어느 필드인지 안 말하고 "validation 실패" 표시 강제.

RFC 7807 — HTTP API 용 Problem Details

IETF 가 error envelope 를 RFC 7807 에 공식화, media type application/problem+json. 필드 다섯:

{
  "type":     "https://example.com/errors/invalid-email",
  "title":    "Invalid email format",
  "status":   400,
  "detail":   "이메일 주소 'pippa@@example.com' 가 유효하지 않습니다.",
  "instance": "/users/42"
}

type 이 error class 식별하는 안정 URI (뒤에 문서 가진 안정 code 로 생각). title 이 짧은 사람 요약. status 가 HTTP status 미러. detail 이 specific 메시지. instance 가 error 난 specific resource.

RFC 7807 은 일부 API (Microsoft, Spring Boot) 가 채택했고 대부분이 무시. 둘 다 동작. 원칙 — 안정 식별자 가진 구조화된 error body — 가 specific format 보다 더 중요.

Client 는 code 에서 분기; 사람은 message 읽음. if error_message == "User not found": ... 하는 client 절대 쓰지 마 — 그 string 이 usability 이유로 바뀔 수 있고, 이제 client 깸. 항상 안정 machine code 에서 분기. 두 필드 존재 이유 정확히 다른 청중 서빙이라서.

아픈 안티패턴

진짜 API 에 나타나면 안 되는 패턴 다섯:

  • {error: ...} 가진 200 OK — Cache 가 성공으로 다룸. Retry 로직 안 켜짐. Monitoring dashboard 가 다 깨졌는데 녹색. 항상 4xx 나 5xx 써.
  • Plain string body. 전체 response 가 "User not found". Client 가 분기 못 함; localizer 가 번역 못 함; support 가 correlate 못 함.
  • Production 의 HTML stack trace. Server 내부 누설 (파일 경로, framework 버전, 환경 변수). 개발용 예약; production 엔 generic 메시지 + on-call 이 찾아볼 request_id 돌려줘.
  • Endpoint 간 일관성 없는 shape. /users{error: ...} 돌려줌; /orders{message: ...}; /payments{detail: ...}. Client 가 포기.
  • Request_id 없음. User 가 error 보고할 때 support 가 해당 server 로그 찾을 방법 없음. 성공 response 에도 항상 request ID 포함해.

cwkPippa 의 error envelope

cwkPippa 는 FastAPI 의 기본 error envelope (4xx/5xx 에 {detail: "..."}, 422 Pydantic 에러에 validation-array) 씀. 처음부터 설계하면 안 일관 — 단일 필드 detail 이 client 한테 machine-readable code 안 줌, request_id 없음. 근데 frontend 가 유일 client 이고 toast 메시지 위해 detail 읽어서 비용 아직 안 물음. 그 날 오면 (제 3자 client, support escalation), migration 이 구조화된 envelope 가진 custom 예외 handler 로 모든 거 감싸기.

Code

FastAPI: code, message, request_id, validation 에러 가진 custom envelope·python
# FastAPI — 구조화 envelope 가진 custom 예외 handler
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
import uuid

app = FastAPI()

class APIError(Exception):
    def __init__(self, status_code: int, code: str, message: str, **extras):
        self.status_code = status_code
        self.code = code
        self.message = message
        self.extras = extras

@app.exception_handler(APIError)
async def api_error_handler(request: Request, exc: APIError):
    request_id = request.headers.get('x-request-id', str(uuid.uuid4()))
    return JSONResponse(
        status_code=exc.status_code,
        content={
            'error': {
                'code': exc.code,
                'message': exc.message,
                'request_id': request_id,
                **exc.extras,
            }
        },
        headers={'X-Request-ID': request_id},
    )

@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
    request_id = request.headers.get('x-request-id', str(uuid.uuid4()))
    errors = [
        {
            'field': '.'.join(str(p) for p in e['loc'][1:]),
            'code': e['type'],
            'message': e['msg'],
        }
        for e in exc.errors()
    ]
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            'error': {
                'code': 'validation_failed',
                'message': f'필드 {len(errors)} 개에 대한 request validation 실패.',
                'errors': errors,
                'request_id': request_id,
            }
        },
        headers={'X-Request-ID': request_id},
    )

# 이제 handler 에서:
@app.get('/users/{uid}')
async def read_user(uid: str):
    user = await db_find(uid)
    if user is None:
        raise APIError(
            status_code=404,
            code='user_not_found',
            message=f'id {uid} 의 user 없음.',
            user_id=uid,
        )
    return user
Client: machine code 가 분기 이끔; message 는 사용자한테 표시·python
# Client — code 에서 분기, 절대 message 에서 안 함
import httpx

def call(method: str, url: str, **kwargs):
    resp = httpx.request(method, url, **kwargs)
    if resp.is_error:
        body = resp.json().get('error', {})
        code = body.get('code')
        if code == 'user_not_found':
            return None  # missing 으로 다룸
        if code == 'validation_failed':
            errors_by_field = {e['field']: e for e in body.get('errors', [])}
            raise UserInputError(errors_by_field)
        if code == 'rate_limited':
            wait = int(resp.headers.get('Retry-After', 1))
            time.sleep(wait)
            return call(method, url, **kwargs)
        # 알 수 없는 error — support escalation 위해 request_id 로그
        raise UnknownAPIError(
            code or f'http_{resp.status_code}',
            body.get('message', resp.text),
            request_id=body.get('request_id'),
        )
    return resp.json()
RFC 7807 Problem Details — IETF 표준 shape 선호하면·json
// RFC 7807 Problem Details — 표준 선호하는 API 용
// Content-Type: application/problem+json

// 단일-error problem
{
  "type":     "https://api.example.com/errors/user-not-found",
  "title":    "User not found",
  "status":   404,
  "detail":   "id u_42 의 user 없음.",
  "instance": "/users/u_42"
}

// 필드 당 detail 가진 validation problem (extension member)
{
  "type":     "https://api.example.com/errors/validation-failed",
  "title":    "Request validation 실패",
  "status":   422,
  "detail":   "필드 3개에 validation 실패.",
  "instance": "/users",
  "errors":   [
    {"field": "email",    "code": "invalid_format"},
    {"field": "password", "code": "too_short", "min": 8},
    {"field": "age",      "code": "out_of_range", "min": 0}
  ]
}

External links

Exercise

여태 만든 FastAPI app 에 error envelope 설계. Custom-envelope 패턴 (code/message/request_id) 이나 RFC 7807 중 골라 — 단 하나 골라서 모든 error response 에 일관 적용. Validation 에러를 필드 당 array 로 감싸는 custom 예외 handler 구현. 그 다음 error.code 에서 분기 (절대 message 에서 안 함) 하고 알 수 없는 error 면 request_id 로그하는 client wrapper 써. 보너스: 다른 error 다섯 (404, 422, 401, 429, 500) 일부러 trigger 해서 각각이 parseable, 유용한 envelope 만드는지 검증.
Hint
FastAPI 의 app.exception_handler decorator 가 모든 error shape custom 가능. Handler 가 HTTPException, RequestValidationError, custom 예외 클래스도 커버하게 해. Request_id 가 middleware 층에서 생성돼서 모든 request — 성공한 것도 — 가 가지게 해; request.state.request_id 에 저장하고 로그. 다섯-error 테스트가 통합 체크: 각각이 envelope shape 존중, client 가 각각을 안 죽고 올바로 처리.

Progress

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

댓글 0

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

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