Pythonの`requests`でファイル送信するときにヘッダーにmultipart/form-dataを直接指定してはいけない

Pythonの`requests`を用いて、ファイル送信する際に`multipart/form-data`をヘッダーに直接指定すると失敗してしまいます。 知らずにはまったので、確認方法を含めて共有のために記載しておきます。
2023.12.06

DA事業本部の横山です。

Pythonのrequestsを用いて、multipart/form-dataのデータを送信する際にヘッダーに指定すると失敗してしまいます。 知らずにはまったので、確認方法を含めて共有のために記載しておきます。

前提条件

本記事で利用している各ライブラリ等のバージョンは以下になります。

  • Python: 3.8.13
  • requests: 2.29.0

概要

  • requestsで、ファイル情報をPOSTする
  • リクエストヘッダへmultipart/form-dataの有無でリクエスト内容の違いを確認する

ファイルを requests でPOSTする

以下のようなソースを用意しました。

import http.client as http_client

import requests

http_client.HTTPConnection.debuglevel = 1

host = "localhost:3000"
endpoint = f"http://{host}/hello"
headers = {
    "Content-Type": "multipart/form-data",
}

params = {
    "id": 1,
}

target_file = open("/tmp/hello.txt", "r")
files = {"file": (target_file.name, target_file.read())}


response = requests.post(url=endpoint, data=params, files=files, headers=headers)
response.raise_for_status()
print(response.json())

ポイントとしては、以下のコードを記載することでrequestsの詳細なログを確認することができます。

import http.client as http_client
http_client.HTTPConnection.debuglevel = 1

以下のヘッダー指定の部分を入れるかコメントアウトを行ってリクエスト内容の違いについて確認していきます。

headers = {
    "Content-Type": "multipart/form-data",
}

リクエスト内容の違い

ソースコード上で、"Content-Type": "multipart/form-data"の指定をコメントアウトした場合、

send: b'POST /hello HTTP/1.1
Host: localhost:3000
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 276
Content-Type: multipart/form-data; boundary=275c6206e4ffc5bc98092f0260014599

'
send: b'--275c6206e4ffc5bc98092f0260014599
Content-Disposition: form-data; name="id"

1
--275c6206e4ffc5bc98092f0260014599
Content-Disposition: form-data; name="file"; filename="/home/yokoyama-takato/work/abaa/test/blog/hello.txt"

hello
--275c6206e4ffc5bc98092f0260014599--
'

ソースコード上で、"Content-Type": "multipart/form-data"の指定を行った場合、

send: b'POST /hello HTTP/1.1
Host: localhost:3000
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: multipart/form-data
Content-Length: 276

'
send: b'--4a5d9783d6c2bc6e5c3b4110b1ab5005
Content-Disposition: form-data; name="id"

1
--4a5d9783d6c2bc6e5c3b4110b1ab5005
Content-Disposition: form-data; name="file"; filename="/home/yokoyama-takato/work/abaa/test/blog/hello.txt"

hello
--4a5d9783d6c2bc6e5c3b4110b1ab5005--
'

二つを比較すると以下のboundary(境界)の値以外に以下の部分が異なっています。

ヘッダー指定なし:Content-Type: multipart/form-data; boundary=275c6206e4ffc5bc98092f0260014599
ヘッダー指定あり:Content-Type: multipart/form-data

ヘッダーに明示的に"Content-Type": "multipart/form-data"の指定を行った場合は、boundaryの値が正しく付与できていないことがわかります。 このため、requestsを利用する際に"Content-Type": "multipart/form-data"を直接指定することは避けましょう。

"Content-Type": "multipart/form-data"詳細については以下のリンクを参照ください。

RFC 7233, セクション 5.4.1

おわりに

リクエスト内容眺めても、ヘッダー部にboundaryが付与されていないことに気が付くのは難しいと思います。 私はプロジェクトメンバに指定いただいて解決したので、おなじようにハマる方が1人でも減ってくれたらうれしいです。

以上になります。この記事がどなたかの助けになれば幸いです。