Post

FastAPI 공식문서 따라하기[22] - Handling Errors

https://fastapi.tiangolo.com/tutorial/handling-errors/ 공식문서 따라하는 글

☑️ Handling Errors

예외처리에 대한 내용이다.

  • 만약 클라이언트가 접근할 수 없는 자원에 접근했거나
  • 존재하지 않는 자원에서 접근을 원하거나
  • 권한 자체가 없는 경우

우리는 에러를 내야한다.

일반적으로 400범위(400 ~ 499)의 에러를 내면서 클라이언트쪽에서 문제가 있음을 알려야 한다.

☑️ Use HTTPException

에러를 포함한 HTTP responses를 클라이언트한테 리턴해야할때가 있다.

이를 리턴하기 위해서는 HTTPException을 임포트해야한다.

1
from fastapi import FastAPI, HTTPException

☑️ Raise an HTTPException in your code

파이썬에서 에러를 직접 발생시키기 위해서는 return이 아닌, raise를 사용해야 한다.

해당 구문을 사용하면서 보안이나 의존성 부분에 명시적으로 눈에 띄기에 더 효율적이다.

다음 예제는 클라이언트가 item의 ID를 보냈는데, 만약 해당 item이 존재하지 않는다면 404에러를 발생시키는 예제이다.

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

app = FastAPI()

items = {"kms" : "Kms is Handsome guy"}

@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 by kms")
    return {"item": items[item_id]}

이렇게 작성하고 kms와 같은 잘못된 요청을 보냈을때는

404에러가 뜰 것이다.

detail안에 내용도 잘 써져있다.

해당 예제에서는 detailstr값을 넣었지만, listdict같은 형태도 보낼 수 있다고 한다.

☑️ Add custom headers

어떤한 문제가 생겼을 경우 헤더에다가 HTTP error를 추가할 수 있다.

위의 코드에서

1
2
3
async def read_item(item_id : str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found by kms", headers={"Kms-error": "There goes KMS error"},)

라고 선언하고, 잘못된 내용을 입력할 경우

이렇게 우리가 설정한 헤더가 들어간걸 볼 수 있다.

☑️ Install custom exception handlers

만약 예외처리를 내가 만들어서 처리하고 싶다면 @app.exception_handler()구문을 통해 처리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

class CustomException(Exception):
    def __init__(self,name: str):
        self.name = name

app = FastAPI()

items = {"kms" : "Kms is Handsome guy"}

@app.exception_handler(CustomException)
async def unvicorn_exception_handelr(request: Request, exc: CustomException):
    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 CustomException(name=name)
    return {"unicorn_name": name}

만약 yolo를 요청으로 보내면 에러가 뜨는 코드이다.

강제로 에러를 발생시키고 이를 exception_handler가 받아서 처리한다.

☑️ Override the default excpetion heandlers

FastAPI는 기본적인 예외처리 기능을 가지고 있다.

유효하지 않은 요청을 받을때 raise를 통해 default JSON Responses를 포함한 HTTPException을 발생시킬 수 있다.

만약 요청에 유효하지 않은 데이터가 포함되어 있다면, FastAPI는 내부적으로 RequestValidationError를 발생시킨다.

RequestValidationError를 임포트하고, 이것을 사용하기 위해서는 @app.excpetion_handler(RequestValidationError)구문을 사용하면 된다.

다음은 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse

app = FastAPI()


@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/kms같은 요청을 보내면 다음과 같이 뜬다.

RequestValidationError는 Pydnantic의 ValidationError의 서브 클래스다.

Pydantic의 response_model을 사용했는데, 데이터 에러가 발생할 시 로그를 통해 오류를 볼 수 있지만 클라이언트, 사용자는 이걸 볼 수 없다.

대신에 500이라는 HTTP status code를 보게 된다. 이는 ValidationError가 코드 어딘가에 있기 때문이라고 한다.

위의 오류에 해당하는 내용을 수정하는 동안에는 클라이언트/사용자는 오류에 대한 내부 정보에 접근할 수 없게 해야한다. 보안 취약성이 노출될 수 있기 때문이다.

☑️ Override the HTTPException error handler

위와 같은 방법으로 HTTPException을 오버라이딩 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from fastapi import FastAPI, HTTPException
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.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/kms같은 요청을 보내면 다음과 같이 뜬다.

☑️ Use the RequestValidationError body

만약 RequestBody로 들어온 값에 유효하지 않은 데이터가 포함되어 있다면 RequestValidationError를 일으킨다.

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
26
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

1
2
3
4
5
6
7
8
curl -X 'POST' \
  'http://localhost:8000/items/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "title": "kms",
  "size": "12d"
}'

이렇게 sizestr을 넣어서 예외를 발생시켜보면,

요로콤 뜬다.

FastAPI’s HTTPException vs Starlette’s HTTPException

FastAPI는 자체적인 HTTPException을 가지고 있다. 이것은 Starlette’s의 HTTPException을 상속받은 것이다.

다른점은 FastAPI의 HTTPException은 response에 헤더를 추가하는것이 허용된다는 것이다. 이는 OAuth 2.0이나 몇몇 보안 툴에 쓰인다고 한다.

고로 FastAPI의 HTTPException을 사용하는게 좋다고 한다.

그러나 만약 예외처리 핸들러가 필요하다면 Starlette’s의 HTTPException을 사용하라고 한다.

왜냐하면 Starlett가 가지고 있는 내부코드나 외부 플러그인을 통해서 Starlette의 HTTPException을 발생시키는데, 이 과정에서 작성한 예외 핸들러를 찾고 실행할 수 있기 때문이라고 한다.

☑️ Re-use FastAPI’s exception handlers

만약 기본 예외처리기를 함께 예외를 사용하려면 from fastapi.exception_handlers를 통해 기본예외처리기를 사용할 수 있다.

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
26
27
28
29
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}

예외처리기를 만들고 그 안에서 또 기본 예외처리를 하고 있다.

나는 어디에 쓰일지 크게 몰라서 적어만 둔다.

This post is licensed under CC BY 4.0 by the author.