エラー処理¶
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
は、APIに関連する追加データを持つ通常のPython例外です。
Pythonの例外であるため、`return`するのではなく、`raise`します。
これはまた、パス操作関数内で呼び出しているユーティリティ関数内にいて、そのユーティリティ関数内からHTTPException
を発生させた場合、パス操作関数の残りのコードは実行されず、そのリクエストはすぐに終了し、HTTPException
からのHTTPエラーがクライアントに送信されることを意味します。
例外を発生させることの利点は、依存関係とセキュリティのセクションでより明らかになります。
この例では、クライアントが存在しない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(「見つかりません」エラー)と以下のJSONレスポンスを受け取ります。
{
"detail": "Item not found"
}
ヒント
HTTPException
を発生させる際、detail
パラメータとして、str
だけでなく、JSONに変換できるあらゆる値を渡すことができます。
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
によって処理されます。
したがって、HTTPステータスコード 418
とJSONコンテンツを含むクリーンなエラーが受信されます。
{"message": "Oops! yolo did something. There goes a rainbow..."}
技術的な詳細
from starlette.requests import Request
および from starlette.responses import JSONResponse
を使用することもできます。
FastAPIは、開発者にとっての利便性として、starlette.responses
と同じものを fastapi.responses
として提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接来ています。Request
も同様です。
デフォルトの例外ハンドラーをオーバーライドする¶
FastAPI には、いくつかのデフォルトの例外ハンドラーがあります。
これらのハンドラーは、HTTPException
を発生させたとき、およびリクエストに無効なデータが含まれているときに、デフォルトの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):
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
の「内部サーバーエラー」を受け取ります。
これは、応答またはコード内の任意の場所(クライアントのリクエストではない)に 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 は、開発者の方々の便宜のために、starlette.responses
と同じものを fastapi.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
vs 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}
この例では、非常に分かりやすいメッセージでエラーを出力しているだけですが、これで意図は伝わるはずです。例外を使用し、その後、デフォルトの例外ハンドラーを再利用できます。