포스트

[FastAPI] <4 : 에러 핸들링>

1. 예외가 필요한 상황들

  1. 클라이언트에게 해당 작업에 대한 권한이 없는 경우.
  2. 클라이언트가 해당 리소스에 엑세스할 수 없는 경우.
  3. 클라이언트가 엑세스할 항목이 항목이 존재하지 않는 경우.

우리는 해당 상황들에 적절한 HTTP 응답 코드 (주로 400번대)를 부여하고 반환한다.

400번대의 HTTP 응답 코드들은 아래 링크에 정리해두었다.

2. HTTPException

2.1 import HTTPException

FastAPI에선 기본적으로 HTTPException을 import해 오류를 관리한다.

1
2
3
4
5
6
7
8
9
10
11
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]}

파이썬에선 오류를 의도적으로 발생시키기 위해 raise 를 사용한다. 해당 코드에선 path로 item_id 를 받는다. 이 경우엔 item/foo 이외로 접속하게 된다면 오류를 띄워준다.

detail에 적어준 문구를 JSON 으로 반환해준다.

2.2 커스텀 헤더

FastAPI에선 커스텀 헤더를 추가로 설정해 오류에 대한 더 많은 정보를 제공할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from fastapi import FastAPI, HTTPException

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]}

Postman으로 확인해보았을때 Header가 제대로 표시된다.

3. 사용자 정의 에러 핸들링

대다수의 경우에는 로직에 따라 원하는 예외를 직접 정의해야 할 것이다.

이러한 경우엔 직접 app에 예외 handler를 추가하는 방식으로 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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}

사용자가 직접 만든 예외(UnicornException)에 Exception을 상속해 예외로 만들어준다.

이후 @app.exception_handler(UnicornException) 을 통해 내가 정의한 예외(UnicornException)가 발생했을 경우 어떻게 작동할 것인지 정의해준다.

이때 Exceptionrequestexc를 매개변수로 받아준다.

해당 코드의 예외를 받아보면 다음과 같다.

그럼 해당 에러 대신 다른 에러를 raise한다면 어떨까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from fastapi import FastAPI, Request, HTTPException
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)
    
    if name == "hi":
        raise HTTPException(status_code=404, detail= f"I hate {name}")
    
    return {"unicorn_name": name}

/unicorns/hi 로 진입하게 된다면 사진과 같은 오류를 띄워준다.

예상한 대로 내가 handler에 등록해 준 오류와 다른 class 이기 때문에 정의한대로 띄워주지 않는다.

4. RequestValidationError의 Body 사용.

FastAPI에서 제공하는 RequestValidationError 의 Body를 사용하면 구체적으로 어떤 입력값 때문에 예외를 발생시켰는지 확인 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

응답 Json의 Body key에서 어떤 값을 입력했는지 보여준다.

5. FastAPI의 예외 handler 재사용

윗 문단에서 직접 JSONResponse로 정의한 것과 다르게 FastAPI에서 제공하는 핸들러를 직접 사용할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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}

이 경우에 /items/3 으로 진입한다면 HTTPException 이 발생할 것이기 때문에

StarletteHTTPException을 handler로 정의한 함수가 실행될 것이다.

/items/3으로 진입해봤다.

그와 다르게 /items/정수가 아닌 것 으로 진입한다면 RequestValidationError 가 발생할 것이기 때문에 지정한 handler의 함수가 실행될 것이다.

/items/ㄹ 로 진입해봤다.

모두 예상대로 실행됐다.



이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.