コンテンツへスキップ

エラー処理

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 は、API に関連する追加データを持つ通常の Python 例外です。

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

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

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

この例では、クライアントが存在しない 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/foo (item_id"foo") をリクエストした場合、クライアントは HTTP ステータスコード 200 と、以下の JSON レスポンスを受け取ります。

{
  "item": "The Foo Wrestlers"
}

しかし、クライアントが http://example.com/items/bar (存在しない item_id"bar") をリクエストした場合、クライアントは HTTP ステータスコード 404 (「見つかりません」エラー) と、以下の JSON レスポンスを受け取ります。

{
  "detail": "Item not found"
}

ヒント

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

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

これらは 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 の同じ例外ユーティリティで追加できます。

たとえば、あなた (または使用しているライブラリ) が発生させる可能性のあるカスタム例外 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 をリクエストすると、*パス操作* は UnicornException を発生させます。

しかし、それは 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 は、開発者であるあなたにとっての利便性として、starlette.responses と同じものを fastapi.responses として提供しています。しかし、利用可能なレスポンスのほとんどは Starlette から直接来ています。Request も同様です。

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

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

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

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

リクエスト検証例外をオーバーライドする

リクエストに無効なデータが含まれている場合、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 validation error
path -> item_id
  value is not a valid integer (type=type_error.integer)

RequestValidationErrorValidationError

Warning

これらは現時点では重要でない場合、スキップしても構わない技術的な詳細です。

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

FastAPI はこれを使用することで、response_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 は、開発者の方々の便宜のために、starlette.responses と同じものを fastapi.responses として提供しています。しかし、利用可能なレスポンスのほとんどは Starlette から直接来ています。

RequestValidationError のボディを使用する

RequestValidationError には、無効なデータとともに受信した body が含まれています。

アプリを開発中に、ボディをログに記録してデバッグしたり、ユーザーに返したりするために使用できます。

from fastapi import FastAPI, Request
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=422,
        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 があります。

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

唯一の違いは、FastAPIHTTPExceptiondetail フィールドに任意の JSON 可能なデータを受け入れるのに対し、Starlette の HTTPException は文字列のみを受け入れる点です。

したがって、コードで通常どおり FastAPIHTTPException を発生させ続けることができます。

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

これにより、Starlette の内部コード、または 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}

この例では、非常に分かりやすいメッセージでエラーを出力しているだけですが、これで何をすべきか理解できるでしょう。例外を使用して、デフォルトの例外ハンドラを再利用できます。