🦕

FastAPIで404エラーを自在にカスタムする

2023/06/24に公開

エラーハンドリングしたい

FastAPIは存在しないURLにリクエストすると
{"detail":"Not Found"}
がデフォルトで返却される。

これを好きなレスポンスに整形して返却してみる。

実装方法

実装前

とりあえず検証コードを作成。
ここからハンドリングを実装していく。

main.py
import uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/hello")
def hello_world() -> dict[str, str]:
    return {"hello": "world"}


def main() -> None:
    uvicorn.run("main:app", port=8000, host="0.0.0.0", reload=True)


if __name__ == "__main__":
    main()

python {モジュール名}でAPIサーバを起動して動作確認。

# /hello GET
$ curl 127.0.0.1:8000/hello
{"hello":"world"}

# /user GET (存在しない)
$ curl 127.0.0.1:8000/user
{"detail":"Not Found"}

/helloは定義したレスポンス
/userは定義していないため404のデフォルトレスポンスになっている。

この状態からエラーハンドリングを実装する。

404エラー実装

公式ドキュメントを参考に実装。
FastAPIは公式ドキュメントが読みやすくて好き☺
https://fastapi.tiangolo.com/tutorial/handling-errors/

公式は自作のハンドラーを使用する方法が記載されている。
今回はHTTPステータス404自体を拾いたいため、以下のように実装してみた。

import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()


@app.get("/hello")
def hello_world() -> dict[str, str]:
    return {"hello": "world"}


# 404エラーを拾う
@app.exception_handler(404)
def not_found(req: Request, exc: HTTPException) -> JSONResponse:
    return JSONResponse(content={"notFound": str(req.url)}, status_code=404)


def main() -> None:
    uvicorn.run("main:app", port=8000, host="0.0.0.0", reload=True)


if __name__ == "__main__":
    main()

@app.exception_handlerのデコレートを使う。
引数は任意のエラー型かintを指定できる。
公式はカスタムハンドラーの型を指定しているが、今回はステータスコードの404を指定
fastapi.responses.JSONResponseにcontentとstatus_codeを指定する。

再度リクエストを送信。

$ curl 127.0.0.1:8000/hello
{"hello":"world"}

$ curl 127.0.0.1:8000/user
{"notFound":"http://127.0.0.1:8000/user"}

404を拾ってくれるのが確認できた。

JSONResponse

content

JSONResponseはcontestに指定したものをjson形式に変換してくれる。
サンプルではdictを指定したがjson形式にできるなら何でも良い。

listは正常に返却してくれる。

# contentは変換できれば何でもOK
@app.exception_handler(404)
def not_found(req: Request, exc: HTTPException) -> JSONResponse:
    return JSONResponse(content=["abc", "def"], status_code=404)
    
# $ curl 127.0.0.1:8000/user
# ["abc","def"]

使う機会は限られそうだが、strだけも大丈夫。

# contentは変換できれば何でもOK
@app.exception_handler(404)
def not_found(req: Request, exc: HTTPException) -> JSONResponse:
    return JSONResponse(content="sample", status_code=404)

# $ curl 127.0.0.1:8000/user
# "sample"

pydantic.BaseModelでスキーマ定義しておくことも可能。
ただし、スキーマをそのまま返却することはできない点に注意。
事前にdict()メソッドで変換してあげる必要がある。

main.py
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

app = FastAPI()

# スキーマ定義
class NotFound(BaseModel):
    msg: str = Field(...)

# NG
@app.exception_handler(404)
def not_found_ng(req: Request, exc: HTTPException) -> JSONResponse:
    return JSONResponse(content=NotFound(msg=str(req.url)), status_code=404)

# OK
@app.exception_handler(404)
def not_found_ok(req: Request, exc: HTTPException) -> JSONResponse:
    return JSONResponse(content=NotFound(msg=str(req.url)).dict(), status_code=404)

# $ curl 127.0.0.1:8000/user
# {"msg":"http://127.0.0.1:8000/user"}
status_code

status_codeにはHTTPステータスを指定できる。

APIでは404エラーを拾うが
実際に返却するときのステータスは400で返却…みたいなことが可能。

# ステータスコード変更
@app.exception_handler(404)
def not_found(req: Request, exc: HTTPException) -> JSONResponse:
    return JSONResponse(content={"notFound": str(req.url)}, status_code=400)

ステータス変更されたか確認してみる。
vscodeを使っているなら拡張機能REST Clientを入れておくと良い。
https://marketplace.visualstudio.com/items?itemName=humao.rest-client

拡張子.httpでリクエストを書ける。

test.http
GET http://localhost:8000/user

リクエストを送信すると、HTTPステータスが400 Bad Requestに変更されている。
リクエスト送信

まとめ

記事中にもリンクを貼っているがFastAPIはドキュメントが優秀で大抵のことは書いてある。

ある特定の処理を書きたい…みたいなことでない限りは
最初に公式を読んでおくと良いと思う!

以上、お疲れ様でした!

参考文献

Discussion