エラーハンドリング¶
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/foo
(item_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
パラメータとして渡すことができます。
dist
やlist
などを渡すことができます。
これらは 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 operationはUnicornException
をraise
します。
しかし、これは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 は開発者の利便性を考慮して、fastapi.responses
と同じstarlette.responses
を提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。これはRequest
と同じです。
デフォルトの例外ハンドラのオーバーライド¶
FastAPI にはいくつかのデフォルトの例外ハンドラがあります。
これらのハンドラは、HTTPException
をraise
させた場合や、リクエストに無効なデータが含まれている場合にデフォルトの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)
RequestValidationError
と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
のボディの使用¶
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
を継承しています。
唯一の違いは、FastAPI のHTTPException
はレスポンスに含まれるヘッダを追加できることです。
これはOAuth 2.0といくつかのセキュリティユーティリティのために内部的に必要とされ、使用されています。
そのため、コード内では通常通り FastAPI のHTTPException
を発生させ続けることができます。
しかし、例外ハンドラを登録する際には、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
しています。
しかし、例外を使用して、デフォルトの例外ハンドラを再利用することができるということが理解できます。