Skip to content

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

  • 먼저, fastapiHTTPException을 임포트한다.


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를 받게 된다.


{
  "item": "The Foo Wrestlers"
}


  • 하지만클라이언트가 http://example.com/items/bar를 요청하는 경우, 해당 클라이언트는 HTTP 상태 코드 404와 다음과 같은 JSON response를 받게 된다.


{
  "detail": "Item not found"
}


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은 UnicornExceptionraise할 것이다.
  • 하지만 unicorn_exception_handler에 의해 예외 처리된다.


  • 따라서 HTTP 상태 코드가 418이고, 다음과 같은 JSON 콘텐츠가 수신된다.


{
  "message": "Oops! yolo did something. There goes a rainbow..."
}


5. Override the default exception handlers

  • FastAPI에는 몇 가지 기본 예외 핸들러가 있다.
  • 이러한 핸들러는 HTTPExceptionraise되고 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


  • 다음과 같이 잘못된 항목을 보낼 수 있다.


{
  "title": "towel",
  "size": "XL"
}


  • 다음과 같이 수신된 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을 모두 사용하기 위해 다음과 같이 이름을 변경한다.


from starlette.exceptions import HTTPException as StarletteHTTPException


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}

References