地味にハマったポイントだったのでメモ。
経緯
外部APIでAuthorizationヘッダを利用する必要があったのでヘッダ取得用関数的な汎用関数を作成して使いまわしていました。
APIは基本的にContent-Typeが「application/json」、一部「application/x-www-form-urlencoded」や「multipart/form-data」が必要という感じだったので、ヘッダー取得用関数は引数でContent-Typeを上書きできるようにしていました。
以下は実際の実装では無い雰囲気コード
import requests
...
def get_headers(content_type='application/json'):
return {
'Authorization': f'Bearer {fetch_bearer()}',
'Content-Type': content_type,
'SecretKey': fetch_secret_key(),
}
...
url = 'https://httpbin.org/post'
files = {'file': open('image.png', 'rb')}
r = requests.post(
url,
files=files,
headers=get_headers(content_type='multipart/form-data')
)
print(r.text)
headersが追加されている以外はrequests 公式のサンプルコードと同じ書き方のはずですが、エラーが発生したという旨のレスポンスが返ってきました。
そのAPIでは認証エラーとなっていたこともあり、ヘッダーのAuthorizationやSecretKeyを生成している関数に何か問題が?と思って調べたのですが、Postmanやcurlで同様の関数で生成したトークンをヘッダーに埋め込み、multipart/form-data形式でAPIにPOSTした場合は、特にエラーは発生しませんでした。
原因
原因はAuthorizationやSecretKeyではなく、Content-Typeの方にありました。
上記のget_headersでは以下のようなヘッダーの辞書が生成されます。
{
'Authorization': 'Bearer xxxxx',
'Content-Type': 'multipart/form-data',
'SecretKey': 'xxxxx',
}
このヘッダーを使用してしまうとContent-Typeにboundaryが付与されません。
boundaryとは、メッセージの複数パートの境界を囲むために使用するもので、multipart/form-dataのデータを送信する際に必要となるものです1。
例えばPostmanでmultipart/form-dataのデータを送信する際のヘッダーはこんな感じで勝手に計算して送信時に付与してくれるようになっています。
requestsの場合も同様で、boundaryの設定はこちらが一々何か計算して設定する必要はなく勝手に付与してくれます。
ただし、今回はContent-Typeを単なる「multipart/form-data」で設定してしまったため、
requestsが付与したデフォルトのContent-Typeが上書きされ、boundaryが無い状態になってしまっていました。
これを解決する方法は簡単で、単純にheadersからContent-Typeを削除するだけです。
(get_headersを書き換えるという手もありますが、multipart/form-dataのAPIが必要だったのはコード上一箇所だけだったのでここはシンプルな方法で)
url = 'https://httpbin.org/post'
files = {'file': open('image.png', 'rb')}
# headerの辞書からContent-Type削除する
headers = get_headers()
headers.pop("Content-Type")
r = requests.post(url, files=files, headers=headers)
print(r.text)
これでエラーは発生しなくなり、正常にデータが送れるようになりました。
知っている人からすれば当たり前では、と思われそうですが普段フォームや便利なクライアントツール経由でデータを送信しているとboundaryは勝手に付与されているものなので意識する機会がなく、結構ハマりました...