FastAPI リクエストの Header/Body など取得方法まとめ

## この記事について

ヘッダとボディ、それからクエリパラメータ、パスパラメータの取得方法をまとめました。他にもクッキーとか色々取得できるものはあるのですが、この記事では先の 4つ (+メモ) にとどめています。

もちろん公式リファレンスはあるのですが、複数ページに分かれていて、コンパクトに 1つにまとめようと思ってこの記事にまとめた次第です。各詳細は[各公式リファレンス](#%E5%8F%82%E7%85%A7%E3%81%97%E3%81%9F%E3%82%82%E3%81%AE)をどうぞ。

レスポンスのまとめはこちら:

https://qiita.com/aKuad/items/d213dc1f7075ab2dd69a

## コード全文

```main.py
# coding: UTF-8

from fastapi import FastAPI #
import uvicorn

app = FastAPI()

@app.post("/")              #
def any_route():            #
  print("any result here")  #
  return {}                 #

if __name__ == "__main__":
  uvicorn.run("main:app", reload=True)
```

以降、記事内では上コードの `#` 部分のみ記述します。

Python スクリプトを実行させた状態で、別ターミナルから curl コマンドで HTTP リクエストを送ります。

## Header

### 全部

```py
from fastapi import FastAPI, Request

@app.post("/")
def any_route(request: Request):
  print(     request.headers )
  print(dict(request.headers)) # 辞書型に変換
  return {}
```

```sh
curl -X POST \
  "http://localhost:8000"
```

### 指定ヘッダのみ

`def` の引数名が取得するヘッダ名の指定になります。(ヘッダ名の `-` は、引数名で `_` と表現するようです。)

:::note
これは以降のクエリパラメータや Form データ等も同様です。
:::

```py
from fastapi import FastAPI, Header

@app.post("/")
def any_route(custom_header: str = Header()):
  print(custom_header)
  return {}
```

```sh
curl -X POST \
  -H "custom-header: header" \
  "http://localhost:8000"
```

`Header()` にデフォルト値を設定できます。これにより当該ヘッダは必須でなく任意になります。

:::note
以降も同様で、デフォルト値を設定した値は任意になります。([詳細は後述](#%E5%BF%85%E9%A0%88%E4%BB%BB%E6%84%8F%E3%81%AE%E6%8C%87%E5%AE%9A-%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%80%A4%E8%A8%AD%E5%AE%9A))
:::

```py
def any_route(custom_header: str = Header("default")):
```

## クエリパラメータ

### 全部

```py
from fastapi import FastAPI

@app.post("/")
def any_route(request: Request):
  print(     request.query_params )
  print(dict(request.query_params)) # 辞書型に変換
  return {}
```

```sh
curl -X POST \
  "http://localhost:8000?query=val"
```

### 指定キーのみ

```py
from fastapi import FastAPI, Query

@app.post("/")
def any_route(query: str = Query()):
  print(query)
  return {}
```

`= Query()` は無くても等価。

```py
def any_route(query: str = Query()):
# 等価
def any_route(query: str):
```

```sh
curl -X POST \
  "http://localhost:8000?query=val"
```

以下のように書くとデフォルト値を設定できます。

```py
def any_route(query: str = Query("default")):
```

```py
def any_route(query: str = "default"):
```

## パスパラメータ

### 全部

```py
from fastapi import FastAPI, Request

@app.post("/{path}")
def any_route(request: Request):
  print(request.path_params) # Header や Query と違い、なぜかこちらは初めから dict
  return {}
```

```sh
curl -X POST \
  "http://localhost:8000/path_name"
```

`@app...` に指定した名前をキーに、入力されたパス名を値に持つ辞書型オブジェクトが得られます。上の例では `{'path': 'path_name'}` という値が得られます。

### 指定パスのみ

```py
from fastapi import FastAPI

@app.post("/{path}")
def any_route(path: str):
  print(path)
  return {}
```

```sh
curl -X POST \
  "http://localhost:8000/path_name"
```

`Header()` や `Query()` と異なり、デフォルト値設定はできません (常に必須値)。パス抜きだと別のルートで処理されて、ルートがなければ 404 エラーになります。

## body

### 全部

```py
from fastapi import FastAPI, Request

@app.post("/")
async def any_route(request: Request):
  print(await request.body())
  return {}
```

```Form の例.sh
curl -X POST \
  -F "key=val" \
  "http://localhost:8000"
```

```JSON の例.sh
curl -X POST \
  -H "content-type: application/json" \
  -d '{"key":"val"}' \
  "http://localhost:8000"
```

:::note warn
`bytes` 型で得られるので、自分で細々した処理をする必要があります。デバッグ用途を除いて推奨できません。
:::

### Form

```py
from fastapi import FastAPI, Form

@app.post("/")
def any_route(form_key: str = Form()):
  print(form_key)
  return {}
```

```sh
curl -X POST \
  -F "form_key=val" \
  "http://localhost:8000"
```

`Form()` にデフォルト値を設定できます。

```py
def any_route(key: str = Form("default")):
```

### Form (ファイル受信)

```py
@app.post("/")
def any_route(form_file: UploadFile = File()):
  print(form_file.filename)    # ファイル名
  print(form_file.file.read()) # ファイルの中身 (bytes 出力)
  return {}
```

```sh
curl -X POST \
  -F "form_file=@any-file.txt" \
  "http://localhost:8000"
```

<details>
<summary>ファイルの中身だけ直接 bytes で得ることも可</summary>

型アノテーションを `bytes` にする。

```py
@app.post("/")
def any_route(form_file: bytes = File()):
  print(form_file)
  return {}
```

この場合、ファイル名や MIME タイプの情報は失われます。

---

</details>

`File()` にデフォルト値を設定でき、これにより当該値は必須でなく任意になります。([詳細は後述](#%E5%BF%85%E9%A0%88%E4%BB%BB%E6%84%8F%E3%81%AE%E6%8C%87%E5%AE%9A-%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%80%A4%E8%A8%AD%E5%AE%9A))

```py
@app.post("/")
def any_route(form_file: UploadFile | None = File(None)):
  if form_file == None:
    form_file = UploadFile(open("any-file.txt", "rb"), filename="any-file.txt")

  print(form_file.filename)
  print(form_file.file.read())
  return {}
```

:::note warn
`File()` に直接 `UploadFile(...)` を書こうとしたら `TypeError: cannot pickle '_io.TextIOWrapper' object` というエラーが発生。なので、一度 `None` を入れる形で対応。
:::

### JSON (スキーマなし)

```py
from fastapi import FastAPI

@app.post("/")
def any_route(json_data: dict):
  print(json_data)
  return {}
```

```JSON の例.sh
curl -X POST \
  -H "content-type: application/json" \
  -d '{"json_key":"val"}' \
  "http://localhost:8000"
```

<details>
<summary>この書き方も一応可 (でもあまりオススメできないかも)</summary>

```py
from fastapi import FastAPI, Request

@app.post("/")
async def any_route(request: Request):
  print(await request.json())
  return {}
```

JSON でないデータが送られたとき、Internal Server Error となってしまいます。また、`Request` の import が必要、`async` にしないといけないなど、少し面倒です。

---

</details>

ヘッダが `application/json` でないとき、またはボディが JSON 構文でないときは、422 エラーが返されます。

```ヘッダが application/json でない.sh
# ボディがあるとき、content-type は デフォルトで application/x-www-form-urlencoded
curl -X POST \
  -d '{"key":"val"}' \
  "http://localhost:8000"
# {"detail":[{"loc":["body"],"msg":"value is not a valid dict","type":"type_error.dict"}]}
# ステータス: 422 Unprocessable Entity
```

```ボディが JSON 構文でない.sh
curl -X POST \
  -H "Content-Type: application/json" \
  -d 'text' \
  "http://localhost:8000"
# {"detail":[{"loc":["body",0],"msg":"Expecting value: line 1 column 1 (char 0)","type":"value_error.jsondecode","ctx":{"msg":"Expecting value","doc":"text","pos":0,"lineno":1,"colno":1}}]}
# ステータス: 422 Unprocessable Entity
```

デフォルト値を設定すると、空ボディでも処理を進行するようにできます。

```py
from fastapi import FastAPI

@app.post("/")
def any_route(json_data: dict = {}):
  print(json_data)
  return {}
```

### JSON (スキーマあり)

スキーマ無しと違い、各値の有無チェックや型チェックをやってくれます。

```py
from fastapi import FastAPI
from pydantic import BaseModel

class JsonScheme(BaseModel):
  #   ~~~~~~~~~~ 任意の名前にできる
  key1: str
  key2: int

@app.post("/")
def any_route(json_body: JsonScheme):
  print(json_body.key1)    # 各値を得る
  print(json_body.key2)    #
  print(json_body.dict())  # 辞書型で得る
  return {}
```

```sh
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"key1":"value1","key2":2}' \
  "http://localhost:8000"
```

`JsonScheme` を以下のように書くとデフォルト値を設定でき、これにより当該の値は必須でなく任意になります。

```py
class JsonScheme(BaseModel):
  key1: str = "default"
  key2: int = 1
```

```sh
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{}' \
  "http://localhost:8000"
# 空でもカッコは必要 (でないと 422 エラーになる)
```

## その他メモ

### 型アノテーションについて

ここまでのコード例で、`def` の引数に対し `str` 型のアノテーションを付けてきました。

```py
@app.post("/")
def any_route(custom_header: str = Header()):
  #                          ~~~ コレ
  print(custom_header)
  return {}
```

無くても動作しますが、あると型チェックをやってくれます。例えば `int` としたとき、整数に変換できない値が来たときにはエラーを返してくれます。

```py
@app.post("/")
def any_route(custom_header: int = Header()):
  print(     custom_header )
  print(type(custom_header))
  return {}
```

```sh
curl -X POST \
  -H "custom-header: header" \
  http://localhost:8000
# {"detail":[{"loc":["header","custom-header"],"msg":"value is not a valid integer","type":"type_error.integer"}]}
# ステータス: 422 Unprocessable Entity
```

### 必須/任意の指定 (デフォルト値設定)

Header 受け取りのコードを例に。

```py
@app.post("/")
def any_route(custom_header: str = Header()):
  print(custom_header)
  return {}
```

上の例で `custom-header` ヘッダのないリクエストに対しては、処理が実行されず、エラー内容の記された JSON と共に 422 レスポンスが返されます。

```sh
curl -X POST \
  http://localhost:8000
# {"detail":[{"loc":["header","custom-header"],"msg":"field required","type":"value_error.missing"}]}
# ステータス: 422 Unprocessable Entity
```

先で示したそれぞれのデフォルト値設定を行うと、当該の値は任意になります。

```py
@app.post("/")
def any_route(custom_header: str = Header("default")):
  print(custom_header)
  return {}
```

```sh
curl -X POST \
  -H "custom-header: header" \
  http://localhost:8000

curl -X POST \
  http://localhost:8000
# いずれでもエラーにならない
```

デフォルト値を `None` にすることで、指定ヘッダの有無を確実に判断できます。

```py
@app.post("/")
def any_route(custom_header: str | None = Header(None)):
  if(custom_header == None):
    print("No header specified")
  else:
    print("custom_header: " + custom_header)
  return {}
```

## 参照したもの

https://fastapi.tiangolo.com/ja/tutorial/header-params/

https://fastapi.tiangolo.com/ja/tutorial/path-params/

https://fastapi.tiangolo.com/ja/tutorial/query-params/

https://fastapi.tiangolo.com/ja/tutorial/body/