コンテンツへスキップ

エラー処理

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 の同じ例外ユーティリティを使用して、カスタム例外ハンドラーを追加できます。

仮に、あなた(または使用しているライブラリ)が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 をリクエストすると、パス操作UnicornExceptionraiseします。

しかし、それはunicorn_exception_handlerによって処理されます。

したがって、HTTPステータスコード 418 とJSONコンテンツを含むクリーンなエラーが受信されます。

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

技術的な詳細

from starlette.requests import Request および from 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)

RequestValidationError vs ValidationError

警告

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

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

FastAPI はこれを使用することで、response_model で Pydantic モデルを使用し、データにエラーがある場合に、ログにエラーが表示されるようにします。

しかし、クライアント/ユーザーには表示されません。代わりに、クライアントはHTTPステータスコード 500 の「内部サーバーエラー」を受け取ります。

これは、応答またはコード内の任意の場所(クライアントのリクエストではない)に 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, 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 vs 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}

この例では、非常に分かりやすいメッセージでエラーを出力しているだけですが、これで意図は伝わるはずです。例外を使用し、その後、デフォルトの例外ハンドラーを再利用できます。