본문 바로가기
Fast API/fastapi배우기

Fast API 배우기 16부 - Handling Errors

by 붕어사랑 티스토리 2021. 11. 2.
반응형

HTTP Exception

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

상기의 코드는 클라이언트가 접근하려는 아이템이 없을경우 404 에러를 일이키는 코드이다.

 

HTTPException은 파이썬 exception에 api와 관련된 데이터를 추가한 exception이다.

그래서 return을 하지 않고 raise를 이용해야한다.

 

exception이 일어날경우 리퀘스트는 그 자리에서 바로 종료되며 이후 코드는 동작하지 않고 클라이언트에게 HTTP error를 전달한다.

 

 

 

성공하였을경우(http://example.com/items/foo), foo가 존재

{
  "item": "The Foo Wrestlers"
}

실패하였을경우(http://example.com/items/bar), bar가 존재하지 않음

{
  "detail": "Item not found"
}

 

 

 

커스텀 헤더 추가하기

HTTP error에 커스텀 헤더를 추가 할 수 있다. 특정 상황에서, 예를들면 보안관련 문제로 이를 이용하는것은 유용하다.

 

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

 

 

커스텀 Exception 만들기

커스텀 exception을 만들 수 있다 사용법은 아래와 같다

 

  • Exception을 상속하는 클래스를 만든다
  • @app.exception_handler() 데코레이션을 사용하여 handler를 등록한다.
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}

 

반응형

 

 

Default Exception handler 오버라이드 하기

fastapi는 exception에 대하여 default handler를 가지고 있다. 이를 오버라이드 하는법은 다음과 같다.

 

아래는 리퀘스트가 invalid한 data를 가지고있을때 일어나는 RequestValidationError exception에 대한 핸들러를 오버라이드 하는 예제이다.

 

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}
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)

 

하는방법은 위와같이 @app.exception_handler 데코레이션에 원하는 handler를 등록해주기만 하면 된다.

 

 

 

 

RequestValidationError vs ValidationError

별로 안중요하니 스킵...

 

RequestValidationError는 Pydantic의 ValidationError를 상속한다. 그리고 에러가 나면 로그에서만 에러가 출력된다.

클라이언트나 유저는 이를 보지 못한다. 오직 Internal Server Error 메세지와 status code 500을 받기만 한다.

 

이러한 이유는 내부적인 데이터가 클라이언트에 노출되면 보안상 취약점이 생기기 때문이다.

 

 

 

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

그럼 아래와 같이 에러와 함께 보내진 데이터가 출력된다.

 

{
  "detail": [
    {
      "loc": [
        "body",
        "size"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ],
  "body": {
    "title": "towel",
    "size": "XL"
  }
}

 

 

 

 

FastAPI's HTTPException vs Startlette's HTTPException

FastAPI는 FastAPI의 HTTP exception을 가지고 있고 이것은 Startlette's의 Exception을 상속 받는다.

 

차이점은 FastAPI's HTTPException은 response에 헤더를 추가할 수 있다는 점이다.

 

단 한가지 주의해야 할 점이 있다.

 

exception handler를 등록할때 Starlette의 HTTPException에다가 등록해야한다.

 

그래야 startlette의 internal code나 extension plugin이 Starlette HTTPException을 일으키면 등록한 핸들러로 처리할 수 있으니깐 말이다.

(라고 공홈에서 설명이 나와있는데 아마 fastapi껀 상속받으니 자동으로 등록한 핸들러를 써서 그런듯? 추측이니 아닐수도 있음을 명시)

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}

 

 

FastAPI의 exception handler 재활용 하기

fastapi의 기본 exception handler를 사용하고 싶다면 아래처럼 import해서 사용하면 된다.

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}

 

반응형

댓글