エラーハンドリング¶
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しています。
しかし、例外を使用して、デフォルトの例外ハンドラを再利用することができるということが理解できます。