エラーハンドリング¶
🌐 Translation by AI and humans
This translation was made by AI guided by humans. 🤝
It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖
You can improve this translation by helping us guide the AI LLM better.
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パラメータとして渡すことができます。
dictや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)と一緒に使用して例外ハンドラをデコレートします。
この例外ハンドラは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: RequestValidationError):
message = "Validation errors:"
for error in exc.errors():
message += f"\nField: {error['loc']}, Error: {error['msg']}"
return PlainTextResponse(message, 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"
}
]
}
以下のテキスト版を取得します:
Validation errors:
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
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: RequestValidationError):
message = "Validation errors:"
for error in exc.errors():
message += f"\nField: {error['loc']}, Error: {error['msg']}"
return PlainTextResponse(message, 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のボディの使用¶
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を持っています。
また、 FastAPIのHTTPExceptionエラークラスはStarletteのHTTPExceptionエラークラスを継承しています。
唯一の違いは、FastAPI のHTTPExceptionはdetailフィールドにJSONに変換可能な任意のデータを受け付けるのに対し、StarletteのHTTPExceptionは文字列のみを受け付けることです。
そのため、コード内では通常通り FastAPI のHTTPExceptionを発生させ続けることができます。
しかし、例外ハンドラを登録する際には、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}
この例では、非常に表現力のあるメッセージでエラーをprintしているだけですが、要点は理解できるはずです。例外を使用し、その後デフォルトの例外ハンドラを再利用できます。