エラー処理¶
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
だけでなく。
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
をリクエストすると、パス操作はUnicornException
をraise
します。
しかし、それはunicorn_exception_handler
によって処理されます。
そのため、418
のHTTPステータスコードと、以下の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
をraise
します。
そして、それに対するデフォルトの例外ハンドラーも含まれています。
それをオーバーライドするには、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
があります。
そして、FastAPIのHTTPException
エラークラスは、StarletteのHTTPException
エラークラスを継承しています。
唯一の違いは、FastAPIのHTTPException
はdetail
フィールドにJSON可能なデータを受け入れるのに対し、StarletteのHTTPException
は文字列のみを受け入れることです。
したがって、コード内で通常どおりにFastAPIのHTTPException
をraise
し続けることができます。
しかし、例外ハンドラーを登録するときは、StarletteのHTTPException
に対して登録する必要があります。
これにより、Starletteの内部コードの一部、またはStarletteの拡張機能やプラグインがStarlette HTTPException
をraise
した場合、ハンドラーがそれをキャッチして処理できるようになります。
この例では、同じコードで両方の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
しているだけですが、考え方はお分かりいただけるでしょう。例外を使用してから、デフォルトの例外ハンドラーを再利用できます。