コンテンツにスキップ

エラーハンドリング

APIを使用しているクライアントにエラーを通知する必要がある状況はたくさんあります。

このクライアントは、フロントエンドを持つブラウザ、誰かのコード、IoTデバイスなどが考えられます。

クライアントに以下のようなことを伝える必要があるかもしれません:

  • クライアントにはその操作のための十分な権限がありません。
  • クライアントはそのリソースにアクセスできません。
  • クライアントがアクセスしようとしていた項目が存在しません。
  • など

これらの場合、通常は 400(400から499)の範囲内の HTTPステータスコード を返すことになります。

これは200のHTTPステータスコード(200から299)に似ています。これらの「200」ステータスコードは、何らかの形でリクエスト「成功」であったことを意味します。

400の範囲にあるステータスコードは、クライアントからのエラーがあったことを意味します。

"404 Not Found" のエラー(およびジョーク)を覚えていますか?

HTTPExceptionの使用

HTTPレスポンスをエラーでクライアントに返すには、HTTPExceptionを使用します。

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

コード内でのHTTPExceptionの発生

HTTPExceptionは通常のPythonの例外であり、APIに関連するデータを追加したものです。

Pythonの例外なので、returnではなく、raiseです。

これはまた、path operation関数の内部で呼び出しているユーティリティ関数の内部からHTTPExceptionを発生させた場合、path operation関数の残りのコードは実行されず、そのリクエストを直ちに終了させ、HTTPExceptionからのHTTPエラーをクライアントに送信することを意味します。

値を返すreturnよりも例外を発生させることの利点は、「依存関係とセキュリティ」のセクションでより明確になります。

この例では、クライアントが存在しないIDでアイテムを要求した場合、404のステータスコードを持つ例外を発生させます:

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

レスポンス結果

クライアントがhttp://example.com/items/fooitem_id "foo")をリクエストすると、HTTPステータスコードが200で、以下のJSONレスポンスが返されます:

{
  "item": "The Foo Wrestlers"
}

しかし、クライアントがhttp://example.com/items/bar(存在しないitem_id "bar")をリクエストした場合、HTTPステータスコード404("not found"エラー)と以下のJSONレスポンスが返されます:

{
  "detail": "Item not found"
}

豆知識

HTTPExceptionを発生させる際には、strだけでなく、JSONに変換できる任意の値をdetailパラメータとして渡すことができます。

distlistなどを渡すことができます。

これらは FastAPI によって自動的に処理され、JSONに変換されます。

カスタムヘッダーの追加

例えば、いくつかのタイプのセキュリティのために、HTTPエラーにカスタムヘッダを追加できると便利な状況がいくつかあります。

おそらくコードの中で直接使用する必要はないでしょう。

しかし、高度なシナリオのために必要な場合には、カスタムヘッダーを追加することができます:

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

カスタム例外ハンドラのインストール

カスタム例外ハンドラはStarletteと同じ例外ユーティリティを使用して追加することができます。

あなた(または使用しているライブラリ)がraiseするかもしれないカスタム例外UnicornExceptionがあるとしましょう。

そして、この例外をFastAPIでグローバルに処理したいと思います。

カスタム例外ハンドラを@app.exception_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}

ここで、/unicorns/yoloをリクエストすると、path operationUnicornExceptionraiseします。

しかし、これはunicorn_exception_handlerで処理されます。

そのため、HTTPステータスコードが418で、JSONの内容が以下のような明確なエラーを受け取ることになります:

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

技術詳細

また、from starlette.requests import Requestfrom starlette.responses import JSONResponseを使用することもできます。

FastAPI は開発者の利便性を考慮して、fastapi.responsesと同じstarlette.responsesを提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。これはRequestと同じです。

デフォルトの例外ハンドラのオーバーライド

FastAPI にはいくつかのデフォルトの例外ハンドラがあります。

これらのハンドラは、HTTPExceptionraiseさせた場合や、リクエストに無効なデータが含まれている場合にデフォルトのJSONレスポンスを返す役割を担っています。

これらの例外ハンドラを独自のものでオーバーライドすることができます。

リクエスト検証の例外のオーバーライド

リクエストに無効なデータが含まれている場合、FastAPI は内部的にRequestValidationErrorを発生させます。

また、そのためのデフォルトの例外ハンドラも含まれています。

これをオーバーライドするにはRequestValidationErrorをインポートして@app.exception_handler(RequestValidationError)と一緒に使用して例外ハンドラをデコレートします。

この例外ハンドラはRequsetと例外を受け取ります。

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 validation error
path -> item_id
  value is not a valid integer (type=type_error.integer)

RequestValidationErrorValidationError

注意

これらは今のあなたにとって重要でない場合は省略しても良い技術的な詳細です。

RequestValidationErrorはPydanticのValidationErrorのサブクラスです。

FastAPIresponse_modelでPydanticモデルを使用していて、データにエラーがあった場合、ログにエラーが表示されるようにこれを使用しています。

しかし、クライアントやユーザーはそれを見ることはありません。その代わりに、クライアントはHTTPステータスコード500の「Internal Server Error」を受け取ります。

レスポンスやコードのどこか(クライアントのリクエストではなく)にPydanticのValidationErrorがある場合、それは実際にはコードのバグなのでこのようにすべきです。

また、あなたがそれを修正している間は、セキュリティの脆弱性が露呈する場合があるため、クライアントやユーザーがエラーに関する内部情報にアクセスできないようにしてください。

エラーハンドラHTTPExceptionのオーバーライド

同様に、HTTPExceptionハンドラをオーバーライドすることもできます。

例えば、これらのエラーに対しては、JSONではなくプレーンテキストを返すようにすることができます:

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}

技術詳細

また、from starlette.responses import PlainTextResponseを使用することもできます。

FastAPI は開発者の利便性を考慮して、fastapi.responsesと同じstarlette.responsesを提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。

RequestValidationErrorのボディの使用

RequestValidationErrorには無効なデータを含む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のHTTPExceptionとStarletteのHTTPException

FastAPIは独自のHTTPExceptionを持っています。

また、 FastAPIのエラークラスHTTPExceptionはStarletteのエラークラスHTTPExceptionを継承しています。

唯一の違いは、FastAPIHTTPExceptionはレスポンスに含まれるヘッダを追加できることです。

これはOAuth 2.0といくつかのセキュリティユーティリティのために内部的に必要とされ、使用されています。

そのため、コード内では通常通り FastAPIHTTPExceptionを発生させ続けることができます。

しかし、例外ハンドラを登録する際には、StarletteのHTTPExceptionを登録しておく必要があります。

これにより、Starletteの内部コードやStarletteの拡張機能やプラグインの一部がHTTPExceptionを発生させた場合、ハンドラがそれをキャッチして処理することができるようになります。

以下の例では、同じコード内で両方のHTTPExceptionを使用できるようにするために、Starletteの例外の名前をStarletteHTTPExceptionに変更しています:

from starlette.exceptions import HTTPException as StarletteHTTPException

FastAPI の例外ハンドラの再利用

また、何らかの方法で例外を使用することもできますが、FastAPI から同じデフォルトの例外ハンドラを使用することもできます。

デフォルトの例外ハンドラを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}

この例では、非常に表現力のあるメッセージでエラーをprintしています。

しかし、例外を使用して、デフォルトの例外ハンドラを再利用することができるということが理解できます。