LoginSignup
5
5

More than 3 years have passed since last update.

Python Fast API ステータスコードやエラーハンドリングについてまとめてみた

Last updated at Posted at 2021-02-14

Response Status Code

  • レスポンスモデルを指定するのと同じ方法で、レスポンスに使用される HTTP ステータスコードを、パス操作のいずれかで status_code というパラメータで宣言することもできます。
from fastapi import FastAPI

app = FastAPI()


@app.post("/items/", status_code=201)
async def create_item(name: str):
    return {"name": name}
  • status_codeパラメータは、HTTPステータスコードを含む数値を受け取ります。
  • status_code は代わりに、Python の http.HTTPStatus のような IntEnum を受け取ることもできます
  • レスポンスでそのステータスコードが返却され、OpenAPI スキーマ(およびユーザーインターフェース)にドキュメント化される。 HTTPでは、レスポンスの一部として3桁の数字のステータスコードを送信します。
  • これらのステータスコードは、それらを認識するために関連付けられた名前を持っていますが、重要な部分は数字です。簡単に言うと
    • 100以上は「情報」のためのものです。直接使用することはほとんどありません。これらのステータスコードを持つレスポンスは本文を持つことができません。
    • 200以上は「成功」の回答です。これらは最もよく使用するものです。
      • 200 はデフォルトのステータスコードで、すべてが「OK」であったことを意味します。
      • 他の例としては、201、"Created "があります。これは、データベースに新しいレコードを作成した後によく使用されます。
      • 特殊なケースとしては、204, "No Content "があります。このレスポンスは、クライアントに返すコンテンツがない場合に使用されます。
    • 300以上は「リダイレクト」です。これらのステータスコードを持つレスポンスにはボディがあってもなくてもかまいませんが、304「Not Modified」を除き、ボディを持つ必要はありません。
    • 400 以上は「クライアントエラー」応答です。これらは、おそらく最もよく使用される 2 番目のタイプです。例として、404は「見つかりませんでした」という応答です。クライアントからの一般的なエラーには、400を使用することができます。
    • 500以上はサーバエラー用です。これらを直接使うことはほとんどありません。アプリケーションコードやサーバのどこかで何か問題が発生した場合、これらのステータスコードのいずれかが自動的に返されます。

Shortcut to remember the names

  • 201は'created'を表す
  • しかし、これらのコードのそれぞれの意味を暗記する必要はありません。fastapi.statusから便利な変数を使うことができます。
from fastapi import FastAPI, status

app = FastAPI()


@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(name: str):
    return {"name": name}

Form Data

  • JSONの代わりにフォームフィールドを受け取る必要がある場合は、Formを使用する
  • フォームを使うには、まず python-multipart をインストールします。
from fastapi import FastAPI, Form

app = FastAPI()


@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
    return {"username": username}

Define Form parameters

  • ボディやクエリの場合と同じ方法でフォームパラメータを作成します。
  • 例えば、OAuth2仕様が使用できる方法の一つ(「パスワードフロー」と呼ばれる)では、ユーザ名とパスワードをフォームフィールドとして送信することが要求されています。
  • この仕様では、フィールドは正確に username と password と名付けられ、JSON ではなくフォームフィールドとして送信されることが要求されています。
  • Form を使えば、Body (および Query, Path, Cookie) と同じメタデータとバリデーションを宣言することができます。
  • FormはBodyを直接継承したクラスです。
  • フォームのボディを宣言するには、明示的に Form を使う必要があります。 ## About "Form Fields"
  • HTMLフォーム()がサーバーにデータを送信する方法は、通常そのデータのために「特別な」エンコーディングを使用しますが、JSONとは異なります。
  • FastAPIは、JSONの代わりに正しい場所からそのデータを読み取るようにします。
  • フォームからのデータは通常、"media type" application/x-www-form-urlencoded を使ってエンコードされます。
  • しかし、フォームがファイルを含む場合、それはmultipart/form-dataとしてエンコードされます。ファイルの扱いについては次の章で説明します。
  • パス操作で複数のFormパラメータを宣言することができますが、リクエストがapplication/jsonではなくapplication/x-www-form-urlencodedを使用してエンコードされたボディを持つことになるので、JSONとして受け取ることを期待しているBodyフィールドも宣言することはできません。これはFastAPIの制限ではなく、HTTPプロトコルの一部です。 ## まとめ フォームデータの入力パラメータを宣言するには Form を使用します。

Request Files

  • クライアントがアップロードするファイルは、Fileを使ってデータ管理します。
  • FileはFormを直接継承するクラスです。しかし、Fastapi から Query, Path, File などをインポートするとき、これらは実際には特別なクラスを返す関数であることを覚えておいてください。
  • Fileのボディを宣言するには、Fileを使用する必要があります。そうしないと、パラメータがクエリパラメータやボディ(JSON)パラメータとして解釈されてしまうからです。
  • ファイルは「form data」としてアップロードされます。
  • パス操作関数のパラメータの型をByteと宣言すると、FastAPIがファイルを読み込んで、内容をByteで受け取ることになります。
  • これは、コンテンツ全体がメモリに保存されることを意味することを覚えておいてください。これは小さなファイルには有効です。
  • UploadFile を使用することで恩恵を受けることができるケースがいくつかあります。 ## File parameters with UploadFile
  • File パラメータを UploadFile 型で定義します。
  • UploadFileを使用すると、Byteよりもいくつかの利点があります。
  • それは、"スプール(処理対象を一時的に置いておく)された "ファイルを使用しています。最大サイズの制限までメモリに保存されたファイルで、この制限を通過した後、それはディスクに格納されます。これは、画像、ビデオ、大きなバイナリなどの大きなファイルでもメモリを消費せずにうまく動作することを意味します。
  • アップロードしたファイルからメタデータを取得できます。ファイルライクな非同期インターフェースを持っています。
  • これは実際の Python SpooledTemporaryFile オブジェクトを公開しており、ファイルライクなオブジェクトを期待する他のライブラリに直接渡すことができます。 ### UploadFile UploadFileには以下の属性があります。
  • filename
    • アップロードされた元のファイル名 (例: myimage.jpg) を持つ文字列。
  • content_type。
    • コンテンツタイプ (MIME タイプ / メディアタイプ) (例: image/jpeg) の str。
  • file:
    • SpooledTemporaryFile(ファイルのようなオブジェクト)。これは実際のPythonファイルで、"file-like "オブジェクトを期待する他の関数やライブラリに直接渡すことができます。

UploadFile には以下の非同期メソッドがあります。これらはすべて、(内部の SpooledTemporaryFile を使用して)その下にある対応するファイルメソッドを呼び出します。
- write(data)。データ(strまたはバイト)をファイルに書き込みます。
- read(size): ファイルのサイズ(int)バイト/文字を読み込みます。ファイルのサイズ(int)バイト/文字を読み込みます。
- seek(offset): ファイルのバイト位置のオフセットを求めます。ファイルのバイト位置のオフセット(int)に移動します。
- 例えば、 await myfile.seek(0) はファイルの先頭に行きます。これは、一度だけ await myfile.read() を実行した後、再度内容を読み込む必要がある場合に特に便利です。
- close() :ファイルを閉じます。
- これらのメソッドはすべて非同期メソッドなので、asyncを使用する必要があります。

What is "Form Data"

  • HTMLフォーム()がサーバーにデータを送信する方法は、通常そのデータのために「特別な」エンコーディングを使用しますが、JSONとは異なります。
  • FastAPIは、JSONの代わりに正しい場所からそのデータを読み取るようにします。
  • フォームからのデータは通常、ファイルを含まない場合、application/x-www-form-urlencodedという "media type "を使ってエンコードされます。
  • しかし、フォームにファイルが含まれている場合、マルチパート/フォームデータとしてエンコードされます。Fileを使用する場合、FastAPIは正しい部分からファイルを取得しなければならないことを知っています。
  • これらのエンコーディングとフォームフィールドの詳細については、POST の MDN ウェブドキュメントを参照してください。
  • パス操作で複数のファイルとフォームパラメータを宣言することができますが、JSONとして受け取ることを期待しているボディフィールドも宣言することはできません。これはFastAPIの制限ではなく、HTTPプロトコルの一部です。

まとめ

  • 入力パラメータとして(フォームデータとして)アップロードするファイルを宣言するにはFileを使用します。

Request Forms and Files

  • ファイルとフォームは同時に定義することができます。
  • ファイルやフォームのパラメータは、ボディやクエリの場合と同じように作成します。
  • ファイルやフォームフィールドがフォームデータとしてアップロードされ、ファイルやフォームフィールドを受け取ることになります。
  • そして、一部のファイルをバイトで、一部のファイルをUploadFileとして宣言することができます。

まとめ

  • 同じリクエストでデータやファイルを受信する必要がある場合は、ファイルとフォームを一緒に使用します。

Handling Errors

  • APIを使用しているクライアントになんのエラーが起きているかを返す必要がある
    • その操作のための権限がありません。
    • リソースにアクセスできません。
    • アクセスしようとしていたアイテムは存在しません。

これらの場合、通常は 400 の範囲 (400 から 499) の HTTP ステータスコードを返します。
- これは、200 の HTTP ステータスコード (200 から 299) に似ています。これらの "200" ステータスコードは、何らかの形でリクエストに "成功" したことを意味します。400の範囲のステータスコードは、クライアントからのエラーがあったことを意味します。

Use HTTPException

  • HTTP レスポンスをクライアントにエラーで返すには、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]}

Raise an HTTPException in your code

  • HTTPExceptionは通常のPythonの例外にAPIに関連するデータを追加したものです。Pythonの例外なので、返却するのではなく、発生させるのです。
  • これはまた、パス操作関数の内部で呼び出しているユーティリティ関数の内部からHTTPExceptionを発生させた場合、パス操作関数の残りのコードを実行するのではなく、そのリクエストをすぐに終了させ、HTTPExceptionからのHTTPエラーをクライアントに送信することを意味しています。値を返すよりも例外を発生させることの利点については、依存関係とセキュリティの項で詳しく説明します。
  • この例では、クライアントが存在しない ID でアイテムをリクエストした場合、ステータスコード 404 で例外を発生させます。 ### The resulting response
  • クライアントが http://example.com/items/foo (item_id "foo") をリクエストした場合、そのクライアントは HTTP ステータスコード 200 と JSON レスポンスを受け取ります。
  • しかし、クライアントが http://example.com/items/bar (存在しない item_id "bar") をリクエストした場合、そのクライアントは HTTP ステータスコード 404 (「見つかりませんでした」エラー) と JSON レスポンスを受け取ります。
  • HTTPException を発生させる際には、str に限らず JSON に変換できる値をパラメータの詳細として渡すことができ、dictやリストなどを渡すことができます。
  • これらはFastAPIによって自動的に処理され、JSONに変換されます。 ## Add custom headers
  • HTTP エラーにカスタムヘッダを追加できると便利な状況がいくつかあります。おそらくコードの中で直接使用する必要はないでしょうが、複雑なAPIを開発したい時にはカスタムヘッダを追加することができます。
@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]}

# response
## response body
{
  "detail": "Item not Found"
}
## response headers
content-length: 27 
content-type: application/json 
date: Sat,13 Feb 2021 04:41:15 GMT 
server: uvicorn 
x-error: There goes my error 

Install custom exception handlers

  • FastAPIではStarlette と同様、例外ユーティリティを使って、カスタム例外ハンドラを追加することができます。
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}
  • 例えば、自分(または使用するライブラリ)が発生させる可能性のあるカスタム例外UnicornExceptionがあるとします。そして、この例外を FastAPI でグローバルに処理したいとします。
  • その時はカスタム例外ハンドラを @app.exception_handler() で追加することができます。
  • ここで、/unicorns/yoloをリクエストすると、パス操作でUnicornExceptionが発生し、unicorn_exception_handler によって処理されます。そのため、HTTPステータスコードが418、JSONが返却されます
  • starlette.requestからRequestをインポートし、starlette.responsからJSONResponseをインポートするという方法もあります。
  • FastAPIは開発者であるあなたの利便性のためだけに、同じstarlette.responsをfastapi.responsと同じように提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接取得できます。Requestも同じです。 ## Override the default exception handlers
  • FastAPI にはいくつかのデフォルト例外ハンドラがあります。これらのハンドラは、HTTPException を発生させた場合や、リクエストに無効なデータがある場合に、デフォルトの JSON レスポンスを返す役割を担っています。これらの例外ハンドラを独自にオーバーライドすることができます。
  • リクエストに無効なデータが含まれている場合、FastAPIは内部的にRequestValidationErrorを発生させます。また、そのためのデフォルトの例外ハンドラも含まれています。
  • これをオーバーライドするには、RequestValidationErrorをインポートし、@app.exception_handler(RequestValidationError)で使用して例外ハンドラを装飾します。例外ハンドラはRequestと例外を受け取ります。 ## Use the RequestValidationError body
  • RequestValidationErrorには、無効なデータで受信したボディが含まれています。
  • アプリの開発中に、ボディをログに記録してデバッグしたり、ユーザーに返したりするために使用できます。
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's HTTPException vs Starlette's HTTPException

  • FastAPIには独自のHTTPExceptionがあります。そして、FastAPIのHTTPExceptionエラークラスはStarletteのHTTPExceptionエラークラスを継承しています。唯一の違いは、FastAPI の HTTPException ではレスポンスにヘッダを追加することができる点です。これは OAuth 2.0 やいくつかのセキュリティユーティリティで内部的に必要/使用されています。そのため、コード内では通常通り FastAPI の HTTPException を発生させ続けることができます。
  • しかし、例外ハンドラを登録する際には、Starlette の HTTPException 用に登録する必要があります。そうすれば、Starlette の内部コードや Starlette の拡張機能やプラグインのいずれかの部分が Starlette HTTPException を発生させた場合でも、ハンドラがそれをキャッチして処理できるようになります。
  • この例では、同じコード内で両方の HTTPException を発生させられるようにするために、Starlette の例外の名前を StarletteHTTPException に変更しています。

Re-use FastAPI's exception handlers

  • また、何らかの方法で例外を使用したい場合は、FastAPIから同じデフォルトの例外ハンドラを使用することもできます。
  • fastapi.exception_handlersからデフォルトの例外ハンドラをインポートして再利用することができます。

Path Operation Configuration

パス操作デコレータを設定するためにパス操作デコレータに渡すことができるパラメータがいくつかあります。

Response Status Code

404のようなコードをパス操作のレスポンスで使用する (HTTP) status_codeとして定義することができます。それぞれの数値コードが何のためのものか覚えていない場合は、statusのショートカット定数を使用することができます。
- そのステータスコードはレスポンスで使用され、OpenAPI スキーマに追加されます。

from typing import Optional, Set

from fastapi import FastAPI, status
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = []


@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    return item

Tags

  • パス操作にタグを追加することができ、パラメータのタグを str のリスト (通常は str を 1 つだけ) で渡します。
  • これらは OpenAPI スキーマに追加され、自動ドキュメント化インターフェースで使用されます。

Summary and description

  • FastAPIでは概要と説明を追加することができます
from typing import Optional, Set

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = []


@app.post(
    "/items/",
    response_model=Item,
    summary="Create an item",
    description="Create an item with all the information, name, description, price, tax and a set of unique tags",
)
async def create_item(item: Item):
    return item

Description from docstring

  • 説明文は複数行に渡って長くなりがちなので、関数のdocstringにパス操作の説明文を宣言すると、そこからFastAPIが読み込んでくれます。
  • docstringにMarkdownを記述すれば、正しく解釈されて表示されます(docstringのインデントを考慮しています)。

Response description

  • レスポンスの説明は、response_descriptionパラメータで指定できます。 ## まとめ
  • パス操作デコレータにパラメータを渡すことで、パス操作のメタデータを簡単に設定・追加することができます。

JSON Compatible Encoder

  • 例えばデータベースに保存する必要がある場合、データ型(Pydanticモデルのようなもの)をJSONと互換性のあるもの(dictやリストなど)に変換する必要がある場合があります。FastAPIのjsonable_encoder()関数を使用します。

Using the jsonable_encoder

  • JSONと互換性のあるデータのみを受け取るfake_dbというデータベースがあるとしましょう。例えば、datetimeオブジェクトはJSONと互換性がないので受け取りません。ですから、datetimeオブジェクトはISOフォーマットのデータを含むstrに変換されなければなりません。
  • 同じように、このデータベースはPydanticモデル(属性を持つオブジェクト)を受け取らず、dictだけを受け取ります。そのような時に jsonable_encoder を使うことができます。jsonable_encoderはPydanticモデルのようなオブジェクトを受け取り、JSON互換のものを返します。
  • この例では、Pydanticモデルをdictに、datetimeをstrに変換します。呼び出した結果は、Python標準のjson.dumps()でエンコードできるものです。
  • これはJSON形式のデータを含む大きな文字列を返しません(文字列として)。これは、JSONと互換性のある値とサブ値を持つPython標準のデータ構造(例えばdict)を返します。
from datetime import datetime
from typing import Optional

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

fake_db = {}


class Item(BaseModel):
    title: str
    timestamp: datetime
    description: Optional[str] = None


app = FastAPI()


@app.put("/items/{id}")
def update_item(id: str, item: Item):
    json_compatible_item_data = jsonable_encoder(item)
    fake_db[id] = json_compatible_item_data

Dependencies - First Steps

  • FastAPIには、非常に強力でありながら直感的に依存性を注入することができる
    • 依存性の注入(いそんせいのちゅうにゅう、英: Dependency injection)とは、コンポーネント間の依存関係をプログラムのソースコードから排除するために、外部の設定ファイルなどでオブジェクトを注入できるようにするソフトウェアパターンである。英語の頭文字からDIと略される。
  • DIを利用したプログラムを作成する場合、コンポーネント間の関係はインタフェースを用いて記述し、具体的なコンポーネントを指定しない。具体的にどのコンポーネントを利用するかは別のコンポーネントや外部ファイル等を利用することで、コンポーネント間の依存関係を薄くすることができる。依存関係がプログラムから外部に取り除かれることで、以下のようなメリットが発生する。
    • 結合度の低下によるコンポーネント化の促進
    • 単体テストの効率化
    • 特定のフレームワークへの依存度低下
  • 非常にシンプルに使用できるように設計されており、開発者が他のコンポーネントをFastAPIに統合するのが非常に簡単になるように設計されています。

First Steps

  • 非常にシンプルな例を見てみましょう。あまりにもシンプルなので、今のところはあまり役に立たないでしょう。しかし、この方法では、依存性注入システムがどのように機能するかに焦点を当てることができます。 ### 依存関係、つまり "依存可能な "ものを作成する まずは依存関係に注目してみましょう。パス操作関数が取れるパラメータと同じものを全て取ることができるだけの関数です。
  • これはすべてのパス操作関数が持っているのと同じ形と構造を持っていて、デコレータのない(@app.get("/some-path")のない)パス操作関数と考えることができます。
  • この場合、この依存関係は以下を期待しています。
    • オプションのクエリパラメータ q は str です。
    • オプションのクエリパラメータ skip は int で、デフォルトは 0 です。
    • オプションのクエリパラメータ limit は int で、デフォルトは 100 です。 そして、これらの値を含む dict を返します。
from typing import Optional

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons
  • Import Depends
  • Declare the dependency, in the "dependant"
    • パス操作関数のパラメータでBodyやQueryなどを使用するのと同じように、新しいパラメータでDependsを使用します。
  • Depends は Body や Query などと同じように関数のパラメータに使用しますが、Depends の動作は少し異なります。Depends に与えるパラメータはひとつだけです。このパラメータは関数のようなものでなければなりません。そして、その関数はパス操作関数と同じようにパラメータを取ります。
  • リクエストのたびに
    • 正しいパラメータを持つ依存関係("依存可能")関数を呼び出す。
    • 関数から結果を取得。
    • その結果をパス操作関数のパラメータに割り当てる
  • この方法では、共通のコードを一度書き、FastAPIがパス操作のための呼び出しに対応する
  • 特別なクラスを作成して、どこかで FastAPI に渡して「登録」する必要はないことに注意してください。
  • Depends に渡すだけで、FastAPIはいい感じにしてくれる。

Integrated with OpenAPI

  • 依存関係(およびサブ依存関係)のすべての要求宣言、検証、および要件は、同じ OpenAPI スキーマに統合されます。したがって、対話型ドキュメントには、これらの依存関係からのすべての情報も含まれます。 ## Simple usage パス操作関数は、パスと操作が一致したときに使用するように宣言されており、FastAPIは正しいパラメータで関数を呼び出し、リクエストからデータを抽出します。実際、すべての(またはほとんどの)Webフレームワークはこのように動作します。これらの関数を直接呼び出すことはありません。それらはフレームワーク(この場合、FastAPI)によって呼び出されます。依存性注入では、パス操作関数もパス操作関数の前に実行されるべき他の何かに「依存」していることを FastAPI に伝えることができます。この同じ考えの「依存性注入」の他の一般的な用語は以下の通りです。
  • resources
  • providers
  • services
  • injectables
  • components

後日加筆修正します

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5