FastAPI — Types Are the Contract
Errors are API contracts too
한국어 원문으로 표시합니다.
목표
업무 예외와 내부 오류를 구분하고 일관된 문제 응답을 제공합니다.
왜 중요한가
클라이언트는 오류가 나면 detail 문자열을 정규식으로 읽고 있었다. 어느 날 문장이 바뀌자 재고 부족도 결제 재시도로 처리됐다. 다른 경로에서는 예외 문자열이 그대로 공개되어 SQL과 내부 경로가 새어 나갔다. 오류는 성공 응답 못지않게 명시적인 계약이 필요하다.
단계
/root/work/fa-problem-lab/service.py에서 DomainError(code, message)는 Exception 하위 클래스이며 .code에 code를 보관합니다. str(예외)는 message입니다.
처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.
mkdir -p /root/work/fa-problem-lab
test -e /root/work/fa-problem-lab/service.py || cp /opt/fixtures/ten_labs/fa-problem-lab/service.py /root/work/fa-problem-lab/service.py
cd /root/work/fa-problem-lab
-
/root/work/fa-problem-lab/service.py에서 status_for(code)는 missing=404, conflict=409, invalid=422, 그 외=500입니다. -
/root/work/fa-problem-lab/service.py에서 public_message(code)는 missing='Resource not found', conflict='State conflict', invalid='Invalid request', 그 외='Internal error'입니다. -
/root/work/fa-problem-lab/service.py에서 request_id(value)는 ASCII 영문·숫자·밑줄·하이픈으로만 이루어진 1~32자 문자열이면 그대로, 아니면 'untracked'입니다. -
/root/work/fa-problem-lab/service.py에서 problem(code, rid)는 type='urn:labhub:problem:'+code, title와 detail=public_message(code), status=status_for(code), request_id=request_id(rid)만 가진 딕셔너리입니다. -
/root/work/fa-problem-lab/service.py에서 response_for(code, rid)는 problem을 본문으로, status_for를 상태로, application/problem+json을 media_type으로, X-Request-ID를 정규화한 rid로 둔 JSONResponse입니다. -
/root/work/fa-problem-lab/service.py에서 install_handlers(app)는 DomainError 핸들러를 등록합니다. 헤더 X-Request-ID를 읽고 exc.code에 대해 response_for를 반환합니다. exc의 message는 응답에 넣지 않습니다. -
/root/work/fa-problem-lab/service.py에서 create_app()은 핸들러를 설치하고 GET /fail/{code}에서 DomainError(code, 내부문장)를 냅니다. 단 code=boom이면 RuntimeError를 냅니다. RuntimeError 핸들러는 code=internal인 고정 500 문제 응답을 반환합니다.
참고
- 인터넷과 패키지 설치 없이 기존 lab-dev 환경에서 수행합니다.
- 각 단계는 45초 채점 예산 안에서 실행됩니다. 실제 sleep이나 네트워크 호출을 추가하지 마세요.
- 채점은 제출 모듈을 새로 불러오고 독립 입력과 임시 DB로 검사합니다. 예상값을 상수로 반환하는 대신 계약을 구현하세요.
- FastAPI 공식 문서 · pytest 공식 문서 · Python sqlite3
- 한계: 이 실습의 문제 응답은 type·title·status·detail·request_id를 갖는 교육용 계약이다. 범용 국제화와 전체 표준 적합성을 주장하지 않는다. request id는 추적에 쓰는 문자열이지 인증 수단이 아니며, 운영 로그에도 비밀값을 그대로 기록해서는 안 된다.
업무 예외에 code를 남긴다
/root/work/fa-problem-lab/service.py에서 DomainError(code, message)는 Exception 하위 클래스이며 .code에 code를 보관합니다. str(예외)는 message입니다.
처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.
mkdir -p /root/work/fa-problem-lab
test -e /root/work/fa-problem-lab/service.py || cp /opt/fixtures/ten_labs/fa-problem-lab/service.py /root/work/fa-problem-lab/service.py
cd /root/work/fa-problem-lab
기계가 판단할 code와 사람이 보는 내부 메시지를 분리합니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/01-contract.sh로 확인하세요.
code를 상태로 매핑한다
/root/work/fa-problem-lab/service.py에서 status_for(code)는 missing=404, conflict=409, invalid=422, 그 외=500입니다.
알 수 없는 code를 성공으로 취급하지 않습니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/02-contract.sh로 확인하세요.
공개 문장을 고정한다
/root/work/fa-problem-lab/service.py에서 public_message(code)는 missing='Resource not found', conflict='State conflict', invalid='Invalid request', 그 외='Internal error'입니다.
예외 문자열에 DB 주소나 내부 경로가 들어 있어도 공개하지 않습니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/03-contract.sh로 확인하세요.
요청 id를 제한한다
/root/work/fa-problem-lab/service.py에서 request_id(value)는 ASCII 영문·숫자·밑줄·하이픈으로만 이루어진 1~32자 문자열이면 그대로, 아니면 'untracked'입니다.
임의 헤더를 반사하지 않도록 길이와 문자 집합을 함께 제한합니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/04-contract.sh로 확인하세요.
문제 본문을 만든다
/root/work/fa-problem-lab/service.py에서 problem(code, rid)는 type='urn:labhub:problem:'+code, title와 detail=public_message(code), status=status_for(code), request_id=request_id(rid)만 가진 딕셔너리입니다.
본문과 HTTP 상태가 서로 다르면 클라이언트가 어느 값을 믿을지 결정할 수 없습니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/05-contract.sh로 확인하세요.
응답 형식을 일관되게 만든다
/root/work/fa-problem-lab/service.py에서 response_for(code, rid)는 problem을 본문으로, status_for를 상태로, application/problem+json을 media_type으로, X-Request-ID를 정규화한 rid로 둔 JSONResponse입니다.
단순 딕셔너리 반환은 오류도 200으로 만들 수 있습니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/06-contract.sh로 확인하세요.
업무 예외 핸들러를 연결한다
/root/work/fa-problem-lab/service.py에서 install_handlers(app)는 DomainError 핸들러를 등록합니다. 헤더 X-Request-ID를 읽고 exc.code에 대해 response_for를 반환합니다. exc의 message는 응답에 넣지 않습니다.
예외를 잡는 위치를 흩어 놓지 말고 앱의 공통 경계에 둡니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/07-contract.sh로 확인하세요.
예상하지 못한 오류도 숨긴다
/root/work/fa-problem-lab/service.py에서 create_app()은 핸들러를 설치하고 GET /fail/{code}에서 DomainError(code, 내부문장)를 냅니다. 단 code=boom이면 RuntimeError를 냅니다. RuntimeError 핸들러는 code=internal인 고정 500 문제 응답을 반환합니다.
테스트에서 예외 재전파를 끄고 실제 500 응답의 바이트를 확인합니다.
저장 후 bash /opt/lab/checks/fa-problem-lab/08-contract.sh로 확인하세요.