21. Handling Errors
1. Handling Errors
- API를 사용하는 클라이언트에게 에러를 알려야 하는 상황은 많이 발생한다.
- 이 클라이언트는 브라우저, 다른 사람의 코드, IoT 장치 등이 될 수 있다.
- 에러가 발생하는 경우 클라이언트에게 다음과 같은 내용을 알려야 한다.
1] 클라이언트에 해당 operation에 대한 권한이 없다는 내용
2] 클라이언트는 해당 리소스에 접근할 수 없다는 내용
3] 클라이언트가 접근하려는 항목이 존재하지 않는다는 내용
4] 기타
- 이와 같은 경우 일반적으로 HTTP 상태 코드 중 400번대를 반환하면 된다.
- 400번대 상태 코드는 클라이언트로부터 발생하는 에러에 대한 상태 코드를 의미한다.
2. Use HTTPException
- 클라이언트에게 에러에 대한 HTTP responses를 반환하려면,
HTTPException
을 사용해야 한다.
1) Import HTTPException
- 먼저,
fastapi
의HTTPException
을 임포트한다.
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
2) Raise an HTTPException
in your code
HTTPException
은 API와 관련된 추가 데이터가 있는 일반적인 Python 예외이기 때문에return
이 아니라raise
를 쓰는 것이다.- 값을 반환하는 것보다 예외를 발생시키는 것이 종속성 및 보안 문제에서 더 이점이 있다.
- 다음의 예제는 클라이언트가 존재하지 않는 ID로 접근하는 경우의 예외 처리를 보여주는 예제이다.
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
3) The resulting response
- 만약 클라이언트가
http://example.com/items/foo
를 요청하는 경우, 해당 클라이언트는 HTTP 상태 코드 200과 다음과 같은 JSON response를 받게 된다.
- 하지만클라이언트가
http://example.com/items/bar
를 요청하는 경우, 해당 클라이언트는 HTTP 상태 코드 404와 다음과 같은 JSON response를 받게 된다.
Note
HTTPException
발생 시str
타입뿐만 아니라 JSON으로 변환할 수 있는 모든 값(dict
타입 및list
타입 등)을 매개변수detail
로 전달할 수 있다.
3. Add custom headers
- HTTP 에러에 사용자 정의 헤더를 추가하는 것이 유용할 때가 있다.
- 일반적으로는 사용되지 않으나, 고급 시나리오에 필요한 경우 추가하기도 한다.
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item_header(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=404,
detail="Item not found",
headers={"X-Error": "There goes my error"},
)
return {"item": items[item_id]}
4. Install custom exception handlers
- Starlette의 동일한 예외 유틸리리티를 사용하여 사용자 정의 예외 처리기를 추가할 수 있다.
- 예를 들어,
UnicornException
이라는 사용자 정의 예외를 추가하여 전역적으로 처리한다고 가정해 보자.
- 이런 경우 다음과 같이
@app.exception_handler()
를 사용하여 사용자 정의 예외 처리기를 추가할 수 있다.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}
- 위의 예제에서
/unicorns/yolo
를 요청하면 path operation은UnicornException
을raise
할 것이다. - 하지만
unicorn_exception_handler
에 의해 예외 처리된다.
- 따라서 HTTP 상태 코드가
418
이고, 다음과 같은 JSON 콘텐츠가 수신된다.
5. Override the default exception handlers
- FastAPI에는 몇 가지 기본 예외 핸들러가 있다.
- 이러한 핸들러는
HTTPException
이raise
되고 request에 잘못된 데이터가 있는 경우 기본 JSON response를 반환하는 역할을 한다. - 이러한 예외 핸들러를 자신만의 것으로 오버라이드할 수 있다.
1) Override request validation exceptions
- request에 잘못된 데이터가 포함된 경우 FastAPI는 내부적으로
RequestValidationError
를 발생시킨다. - 또한 이에 대한 기본 예외 핸들러가 포함되어 있다.
- 이를 오버라이드하려면
RequestValidationError
를 임포트한 후@app.exception_handler(RequestValidationError)
로 예외 핸들러를 만들면 된다.
- 다음의 예제는 예외 핸들러가
Request
및 예외를 수신하는 예제이다.
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
- 위의 코드를 실행한 후
/items/foo
로 이동하면 다음과 같은 기본 JSON 에러가 발생하지 않는다.
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
(1) RequestValidationError
vs ValidationError
RequestValidationError
는 Pydantic의ValidationError
의 하위 클래스이다.- FastAPI는 이를 사용하므로,
response_model
에서 Pydantic 모델을 사용하고 데이터에 에러가 있는 경우 로그에 에러가 표시된다. - 하지만 클라이언트는 그 에러 내용을 볼 수 없다.
- 그 대신 클라이언트는 HTTP 상태 코드
500
과 함께 "Internal Server Error"를 수신한다. - 이런 식으로 작동해야만 클라이언트가 수신하는 response에 Pydantic의
ValidationError
가 노출되지 않는다. - 개발자가 버그를 고치는 동안 클라이언트는 보안 취약점을 노출할 수 있으므로 에러에 대한 내부 정보에 접근할 수 없어야만 한다.
2) Override the HTTPException error handler
- 위의 예제와 같이
HTTPException
핸들러를 오버라이드할 수 있다.
- 다음의 예제는 에러에 대해 JSON 대신 일반 텍스트 response를 반환하는 예제이다.
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
3) Use the RequestValidationError
body
RequestValidationError
에는 잘못된 데이터를 받는body
를 포함되어 있다.
- 이를 이용하여 다음과 같이 앱을 개발하는 동안 body를 기록하고, 디버그하고 클라이언트에게 반환할 수 있다.
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
)
class Item(BaseModel):
title: str
size: int
@app.post("/items/")
async def create_item(item: Item):
return item
- 다음과 같이 잘못된 항목을 보낼 수 있다.
- 다음과 같이 수신된 body를 포함하는 데이터가 유효하지 않다는 response를 받게 된다.
{
"detail": [
{
"loc": ["body", "size"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": "towel",
"size": "XL"
}
}
(1) FastAPI's HTTPException
vs Starlette's HTTPException
- FastAPI에는 자체적인
HTTPException
이 있다. - 그리고 FastAPI의
HTTPException
에러 클래스는 Starlette의HTTPException
의 에러 클래스를 상속받는다. - 이 둘의 유일한 차이점은 FastAPI의
HTTPException
을 사용하면, response에 포함할 headers를 추가할 수 있다는 것이다. - 이는 OAuth 2.0 및 일부 보안 유틸리티에 내부적으로 필요하다.
- 따라서 위의 예제와 같이 FastAPI의
HTTPException
을 발생시키는 것이 좋다. - 하지만 HTTP 예외 핸들러를 정의할 때는 데코레이터의 인수로는 Starlette의
HTTPException
만 가능하다. - 이렇게 해야만 Starlette의 내부 코드 또는 Starlette의 플러그인의 일부가 Starlette
HTTPException
을 발생시키는 경우 예외 핸들러가 이를 포착하고 처리할 수 있다.
- FastAPI의
HTTPException
과 Starlette의HTTPException
을 모두 사용하기 위해 다음과 같이 이름을 변경한다.
4) Re-use FastAPI's exception handlers
fastapi.exception_handlers
에서 기본 예외 핸들러를 가져와 재사용할 수 있다.
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {repr(exc)}")
return await http_exception_handler(request, exc)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
print(f"OMG! The client sent invalid data!: {exc}")
return await request_validation_exception_handler(request, exc)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}