コンテンツにスキップ

エラー処理

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 を raise する

HTTPException は、API に関連する追加データを持つ通常の Python 例外です。

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

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

値を return するのではなく、例外を raise する利点は、依存関係とセキュリティに関するセクションでより明確になります。

この例では、クライアントが存在しない ID でアイテムをリクエストした場合、ステータスコード 404 の例外を raise します

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 を raise する場合、detail パラメーターとして JSON に変換できる任意の値を渡すことができます。str だけでなく。

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によって処理されます。

そのため、418のHTTPステータスコードと、以下の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は内部でRequestValidationErrorraiseします。

そして、それに対するデフォルトの例外ハンドラーも含まれています。

それをオーバーライドするには、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の「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 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"
}

受信したbodyを含む、データが無効であることを示すレスポンスを受け取ります。

{
  "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は文字列のみを受け入れることです。

したがって、コード内で通常どおりにFastAPIHTTPExceptionraiseし続けることができます。

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

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

この例では、同じコードで両方の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しているだけですが、考え方はお分かりいただけるでしょう。例外を使用してから、デフォルトの例外ハンドラーを再利用できます。