10 KiB
에러 처리
API를 사용하는 클라이언트에게 에러를 알려야 하는 상황이 많습니다.
클라이언트는 프론트엔드가 있는 브라우저일 수도 있고, 다른 사람의 코드일 수도 있고, IoT 장치일 수도 있습니다.
다음과 같은 상황을 클라이언트에게 알려야 할 수 있습니다:
- 해당 작업에 대한 권한이 없음
- 해당 리소스에 접근할 수 없음
- 접근하려는 항목이 존재하지 않음
- 기타 등등
이런 경우, 보통 400번대(400~499)의 HTTP 상태 코드를 반환합니다.
이는 200번대 HTTP 상태 코드(200~299)와 비슷합니다. "200"번대 상태 코드는 요청이 어떻게든 "성공"했다는 의미입니다.
400번대 상태 코드는 클라이언트에서 에러가 발생했다는 의미입니다.
"404 Not Found" 에러(그리고 농담들)를 기억하시나요?
HTTPException 사용하기
에러가 있는 HTTP 응답을 클라이언트에 반환하려면 HTTPException을 사용합니다.
HTTPException 임포트하기
{* ../../docs_src/handling_errors/tutorial001.py hl[1] *}
코드에서 HTTPException 발생시키기
HTTPException은 API 관련 추가 데이터를 담은 일반 파이썬 예외입니다.
파이썬 예외이므로 return이 아닌 raise를 사용합니다.
경로 작동 함수 내에서 호출하는 유틸리티 함수가 있다면, 그 함수 안에서 HTTPException을 발생시켰을 때 경로 작동 함수의 나머지 코드는 실행되지 않고, 요청이 즉시 종료되며 HTTPException의 HTTP 에러가 클라이언트로 전송됩니다.
값을 반환하는 것보다 예외를 발생시키는 것의 이점은 의존성과 보안 섹션에서 더 명확해질 것입니다.
이 예제에서는 클라이언트가 존재하지 않는 ID로 항목을 요청하면 상태 코드 404의 예외를 발생시킵니다:
{* ../../docs_src/handling_errors/tutorial001.py hl[11] *}
결과 응답
클라이언트가 http://example.com/items/foo(item_id가 "foo")를 요청하면, HTTP 상태 코드 200과 다음 JSON 응답을 받습니다:
{
"item": "The Foo Wrestlers"
}
하지만 클라이언트가 http://example.com/items/bar(존재하지 않는 item_id "bar")를 요청하면, HTTP 상태 코드 404("not found" 에러)와 다음 JSON 응답을 받습니다:
{
"detail": "Item not found"
}
/// tip | 팁
HTTPException을 발생시킬 때 detail 매개변수에 str뿐만 아니라 JSON으로 변환 가능한 모든 값을 전달할 수 있습니다.
dict, list 등을 전달할 수 있습니다.
이들은 FastAPI가 자동으로 처리하여 JSON으로 변환합니다.
///
커스텀 헤더 추가하기
HTTP 에러에 커스텀 헤더를 추가하는 것이 유용할 때가 있습니다. 예를 들어, 일부 보안 시나리오에서 말이죠.
직접 사용할 필요는 거의 없습니다.
하지만 필요한 고급 시나리오를 위해 커스텀 헤더를 추가할 수 있습니다:
{* ../../docs_src/handling_errors/tutorial002.py hl[14] *}
커스텀 예외 핸들러 설치하기
Starlette의 예외 유틸리티를 사용하여 커스텀 예외 핸들러를 추가할 수 있습니다.
여러분(또는 사용하는 라이브러리)이 raise할 수 있는 커스텀 예외 UnicornException이 있다고 가정해봅시다.
이 예외를 FastAPI에서 전역으로 처리하고 싶습니다.
@app.exception_handler()로 커스텀 예외 핸들러를 추가할 수 있습니다:
{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *}
여기서 /unicorns/yolo를 요청하면 경로 작동이 UnicornException을 raise합니다.
하지만 unicorn_exception_handler가 이를 처리합니다.
따라서 HTTP 상태 코드 418과 다음 JSON 내용의 깔끔한 에러를 받습니다:
{"message": "Oops! yolo did something. There goes a rainbow..."}
/// note | 기술적 세부사항
from starlette.requests import Request와 from starlette.responses import JSONResponse를 사용할 수도 있습니다.
FastAPI는 개발자 편의를 위해 starlette.responses를 fastapi.responses로도 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 가져옵니다. Request도 마찬가지입니다.
///
기본 예외 핸들러 오버라이드하기
FastAPI에는 몇 가지 기본 예외 핸들러가 있습니다.
이 핸들러들은 HTTPException을 raise하거나 요청에 유효하지 않은 데이터가 있을 때 기본 JSON 응답을 반환합니다.
이러한 예외 핸들러를 여러분만의 것으로 오버라이드할 수 있습니다.
요청 검증 예외 오버라이드하기
요청에 유효하지 않은 데이터가 포함되면 FastAPI는 내부적으로 RequestValidationError를 발생시킵니다.
그리고 기본 예외 핸들러도 포함하고 있습니다.
이를 오버라이드하려면 RequestValidationError를 임포트하고 @app.exception_handler(RequestValidationError)로 예외 핸들러를 데코레이트합니다.
예외 핸들러는 Request와 예외를 받습니다.
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *}
이제 /items/foo로 가면 다음과 같은 기본 JSON 에러 대신:
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
다음과 같은 텍스트 버전을 받습니다:
1 validation error
path -> item_id
value is not a valid integer (type=type_error.integer)
RequestValidationError vs ValidationError
/// warning | 주의
지금 당장 중요하지 않다면 건너뛸 수 있는 기술 세부사항입니다.
///
RequestValidationError는 Pydantic의 ValidationError의 서브클래스입니다.
FastAPI는 이를 사용하여 response_model에서 Pydantic 모델을 사용하고 데이터에 에러가 있는 경우 로그에서 에러를 볼 수 있게 합니다.
하지만 클라이언트/사용자는 이를 보지 못합니다. 대신 HTTP 상태 코드 500과 함께 "Internal Server Error"를 받습니다.
응답이나 코드 어딘가에(클라이언트 요청이 아닌) Pydantic ValidationError가 있다면 실제로는 코드의 버그이므로 이렇게 되어야 합니다.
그리고 이를 수정하는 동안 클라이언트/사용자는 에러에 대한 내부 정보에 접근할 수 없어야 합니다. 보안 취약점을 노출할 수 있기 때문입니다.
HTTPException 에러 핸들러 오버라이드하기
같은 방식으로 HTTPException 핸들러를 오버라이드할 수 있습니다.
예를 들어 이런 에러들에 대해 JSON 대신 일반 텍스트 응답을 반환하고 싶을 수 있습니다:
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *}
/// note | 기술적 세부사항
from starlette.responses import PlainTextResponse를 사용할 수도 있습니다.
FastAPI는 개발자 편의를 위해 starlette.responses를 fastapi.responses로도 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 가져옵니다.
///
RequestValidationError 본문 사용하기
RequestValidationError는 유효하지 않은 데이터와 함께 받은 body를 포함합니다.
앱 개발 중 이를 사용하여 본문을 로깅하고 디버그하거나 사용자에게 반환할 수 있습니다.
{* ../../docs_src/handling_errors/tutorial005.py hl[14] *}
이제 다음과 같이 유효하지 않은 항목을 보내보세요:
{
"title": "towel",
"size": "XL"
}
데이터가 유효하지 않다는 내용과 받은 본문을 포함한 응답을 받습니다:
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": "towel",
"size": "XL"
}
}
FastAPI의 HTTPException vs Starlette의 HTTPException
FastAPI는 자체 HTTPException을 가지고 있습니다.
FastAPI의 HTTPException 에러 클래스는 Starlette의 HTTPException 에러 클래스를 상속받습니다.
유일한 차이점은 FastAPI의 HTTPException은 detail 필드에 JSON으로 변환 가능한 모든 데이터를 허용하는 반면, Starlette의 HTTPException은 문자열만 허용한다는 것입니다.
따라서 여러분의 코드에서 평소처럼 FastAPI의 HTTPException을 계속 발생시킬 수 있습니다.
하지만 예외 핸들러를 등록할 때는 Starlette의 HTTPException에 대해 등록해야 합니다.
이렇게 하면 Starlette 내부 코드나 Starlette 확장/플러그인에서 Starlette HTTPException을 발생시켰을 때 여러분의 핸들러가 이를 잡아서 처리할 수 있습니다.
이 예제에서는 같은 코드에서 두 HTTPException을 모두 사용하기 위해 Starlette의 예외를 StarletteHTTPException으로 이름을 바꿨습니다:
from starlette.exceptions import HTTPException as StarletteHTTPException
FastAPI의 예외 핸들러 재사용하기
예외와 함께 FastAPI의 기본 예외 핸들러를 사용하고 싶다면 fastapi.exception_handlers에서 기본 예외 핸들러를 임포트하여 재사용할 수 있습니다:
{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *}
이 예제에서는 아주 설명적인 메시지로 에러를 출력하지만, 핵심은 이해할 수 있을 것입니다. 예외를 사용한 다음 기본 예외 핸들러를 재사용하면 됩니다.