Handling Errors
내 API 사용자에게 에러발생을 알려줘야하는 많은 상황이 있다.
이 사용자는 프론트엔드 브라우저 유저나 사물인터넷 사용자 등등이 될 수 있다.
우리는 다음과 같은 상황에서 에러발생을 사용자에게 알려야 한다.
- 사용자는 operation을 실행할 충분한 권한이 없다.
- 사용자는 자원에 접근할 권한이 없다.
- 사용자가 접근하려는 item이 존재하지 않는다.
- etc.
이러한 상황에서 우리는 일반적으로 HTTP 상태코드 400~499를 반환해줘야 한다.
HTTP 상태코드 200~299가 '성공'의 의미를 담고있는것과는 반대이다.
HTTP 상태코드 400~499는 error가 존재함을 의미한다.
"404 Not Found" error를 본적이 있다면 이해하기 쉽다.
다음 예시들을 통해, 예상되거나 예상할 수 없는 에러의 처리방법을 알아보자.
1. Use HTTPException
HTTPException을 사용해 error를 담은 response를 보낼 수 있다.
1-1. Import HTTPException
from fastapi import FastAPI, HTTPException
1-2. Raise an HTTPException in your code
사실 HTTPException 은 API와 연관된 부가적 data를 담고있는 일반 Python 예외이다.
Python 예외이기 때문에, return 하지 말고 raise 해야 한다.
이는 경로 연산 함수 속 어떠한 함수 내부에서 에러가 발생한다면, 에러를 전송한 뒤 경로 연산함수의 나머지 코드를 실행하지 않고 바로 종료한다는 의미를 가지고 있다.
종속성 및 보안 부분에서 return 보다 raise 는 분명한 이점을 가진다.
다음 예제에서, 클라이언트가 존재하지 않는 ID를 요청하고, 이에 따른 예외가 발생한다(404).
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]}
이 예제에서 상기 서술한 단락의 의미를 알 수 있다.
1-2-1. The resulting response
위 예제method의 결과는 다음과 같다.
클라이언트가 다음과 같은 요청을 보내면
http://example.com/items/foo
foo라는 item_id가 존재하므로
다음과 같은 JSON response를 반환해준다.
(HTTP 상태 코드는 200)
{
"item": "The Foo Wrestlers"
}
그러나 클라이언트가 아래와 같이 존재하지 않는 item에 대한 정보를 요청하면
http://example.com/items/bar
bar이라는 item_id는 존재하지 않으므로
다음과 같은 JSON response를 반환해준다.
(HTTP 상태 코드는 404)
{
"detail": "Item not found"
}
2. Add custom headers
HTTP error에 사용자 지정 header를 추가하는게 유용한 상황이 있다.
아마도 모든 코드에 직접 사용할 필요는 없고, 고급 상황을 대비해 사용자 지정 header를 추가할 수 있다.
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items-header/{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]}
2-1. Install custom exception handlers
<H1> 2와 같은 상황의 예외를 처리하기 위해
발생시킬수도 있는 UnicornException이라는 사용자 지정 예외를 만들어보자.
그리고 이 예외를 FastAPI 전역에서 처리하고 싶다면, @app.exception_handler() 라는 데코레이터를 예외 function에 추가해주자.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@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에 의해 처리될 것이다.
그래서 요청에 해당하는 response는 명확한 error이다(HTTP status code of 418).
이 에러가 담고있는 JSON 객체는 다음과 같다.
{"message": "Oops! yolo did something. There goes a rainbow..."}
3. Override the default exception handlers
FastAPI는 default(기본) 예외 처리기를 가지고 있다.
이 처리기는 HTTPException을 발생시키거나 유효하지 않은 data를 요청받았을 때 JSON response를 돌려줄 책임이 있다.
우리는 이 default handler들을 override(재정의)하여 내 방식대로 사용할 수 있다!
3-1. Override request validation exceptions
유효하지 않은 data를 요청받았을 때, FastAPI는 내부적으로 RequestValidationError 를 발생시킨다.
그리고 이 예외를 위한 기본 처리기도 존재한다.
내 예외 처리기에 다음과 같은 데코레이터를 써주는 것이다.
@app.exception_handler(RequestValidationError)
일반적으로, 다음과 같은 예시를 통해 이러한 기본 처리기를 재정의할 수 있다.
우선 관련 라이브러리 import부터 하고
from fastapi.exceptions import RequestValidationError
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}
item_id 가 foo인 정보를 요청하면, 다음과 같은 JSON error를 가져오는 대신,
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
문자 기반 버전의 response를 볼 수 있다!
1 validation error
path -> item_id
value is not a valid integer (type=type_error.integer)
참고로, starlette.responses를 fastapi.responses과 똑같게 사용할 수 있다.
3-2. Override the HTTPException error handler
위와 같은 방법으로, HTTPException 도 재정의(Override)가능하다.
다음은 어떤 예외에 대한 JSON contents 대신 plain text를 받게 해주는 예제코드이다.
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}
4. Use the RequestValidationError body
RequestValidationError 은 유효하지 않은 데이터를 가지고 있는 body도 포함해 가지고 있다.
이런 정보를 버리지 않고 가지고 있는 이유는, 에러가 발생한 body에 접근하고 debug하며 사용자에게 요청한 정보를 다시 돌려줄 때 사용될 수 있다(더 많은 용례가 있다).
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
위 예제에서, 다음과 같은 유효하지 않은 요청을 보내면,
{
"title": "towel",
"size": "XL"
}
다음과 같이 received된 body가 유효하지 않은 데이터를 포함하고 있음을 말해주는 response를 받게 된다.
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": "towel",
"size": "XL"
}
}
5. FastAPI's HTTPException vs Starlette's HTTPException
FastAPI만의 HTTP예외는 Starlette의 HTTP예외 클래스를 상속하고 있다.
두 가지 예외처리 라이브러리의 유일한 차이는, FastAPI는 response속에 포함된 header를 추가할 수 있도록 허용해 준다는 것이다.
이 때문에, FastAPI's HTTPException을 내 코드속에서 정상적으로 꾸준히 발생시킬 수 있다.
그러나 Exception Handler를 등록할 때는 Starlette의 HTTPException에 등록해야 한다.
이렇게 하면 Starlette의 내부 코드의 일부, 또는 Starlette 확장자 또는 플러그인이 Starlette HTTP 예외를 발생시킬 경우 핸들러가 이를 포착하여 처리할 수 있다.
이 예에서는 동일한 코드에 두 HTTPException 을 모두 가질 수 있도록 Starlette's exceptions가 StarletteHTTPException 로 재명명된다.
from starlette.exceptions import HTTPException as StarletteHTTPException
* as의 의미에 focus on
6. Re-use FastAPI's exception handlers
예외를 FastAPI의 동일한 기본 예외 처리기와 함께 사용하려면 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}
위의 예제는 OMG 메시지와 함께 오류를 출력하고 있다.
위처럼 예외를 사용한 다음 기본 예외 처리기를 다시 사용할 수 있다.
7. Reference
https://fastapi.tiangolo.com/tutorial/handling-errors/
Handling Errors - FastAPI
Handling Errors Warning The current page still doesn't have a translation for this language. But you can help translating it: Contributing. There are many situations in where you need to notify an error to a client that is using your API. This client could
fastapi.tiangolo.com
'프레임워크 > FastAPI' 카테고리의 다른 글
[FastAPI] FastAPI [22] JSON Compatible Encoder (0) | 2023.05.23 |
---|---|
[FastAPI] FastAPI [21] Path Operation Configuration (0) | 2023.05.16 |
[FastAPI] FastAPI [19] Request Forms and Files (0) | 2023.05.09 |
[FastAPI] FastAPI [18] Request Files (0) | 2023.05.09 |
[FastAPI] FastAPI [17] Form data (0) | 2023.05.09 |