この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
CX事業本部@大阪の岩田です
PythonのライブラリRequestsを利用してPOSTリクエストを発行する際リクエストヘッダからContent-Type
を削除するという一風変わった要件に対応したので、実装手順をメモとして残しておきます
背景
Pythonのコードから外部のAPIを呼び出す必要があったのですが、そのAPIの仕様が
- HTTPメソッドはPOST
- クエリストリングを使用している
- リクエストボディも利用している
- リクエストボディは
application/x-www-form-urlencoded
形式でセットする必要がある
- リクエストボディは
という仕様でした。これだけなら良かったのですが、このAPIは「リクエストヘッダにContent-Type
がセットされていると正常に処理できない」という問題を抱えていました。外部APIの実装詳細は不明なのですが、どうもJavaのServletRequestの使い方がマズそうとのことです。
HTTP POST リクエストで発生するなど、パラメーターデータがリクエスト本文で送信された場合、
getInputStream()
またはgetReader()
を介して直接本文を読み取ると、このメソッドの実行が妨げられる可能性があります。
Content-Typeに関してはRFC7231で
A sender that generates a message containing a payload body SHOULD generate a Content-Type header field in that message unless the intended media type of the enclosed representation is unknown to the sender.
と定義されており、本来は外部API側でContent-Type
を正しく処理できるよう修正を加えてもらうべきなのですが、一旦API呼び出し側(Python)でContent-Type
無しのリクエストを生成して対応することになりました。
環境
今回使用した各バージョンは以下の通りです
- Python: 3.8.3
- Requests: 2.23.0
Requestsの高レベルなAPIを使用したPOST
API呼び出し側のPythonはHTTPリクエストを実行するためにRequestsライブラリを使用していました。Requestsライブラリを用いててクエリストリングとリクエストボディをセットしたPOSTリクエストは以下のようなコードで実現できます。
>>> import requests
>>>
>>> res = requests.post('http://localhost',data={"key":"val"},params={"query":"hogehoge"})
ただし、このコードはライブラリ側で自動的にContent-Type
がセットされてしまいます。ApacheのDockerイメージを起動した状態でtcpdumpでパケットをキャプチャしながら上記のコードを実行すると以下のように出力されました。
POST /?query=hogehoge HTTP/1.1
Host: localhost
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 7
Content-Type: application/x-www-form-urlencoded
key=val
コード上は明示的にHTTPヘッダをセットしていませんが、リクエストヘッダがいくつか自動的に付与されていることが分かります。前述の通りRFC7231でContent-Type
はセットすべきと定義されているので、ライブラリの振る舞いとしては至極まっとうなのですが、今回は特殊な要件があるのでContent-Type
の自動設定を回避したいです。
Requestsのちょっと低レベルなAPIを使用したPOST
シンプルにrequests.post
するとContent-Type
が自動でセットされてしまうので、今度はリクエストヘッダを自分で加工してみます。ライブラリに内包されているPreparedRequest
オブジェクトを利用してHTTPリクエストを生成し、Session
オブジェクトからリクエストを実行します。コードは以下のようになります。
>>> from requests import PreparedRequest, Session
>>>
>>> req = PreparedRequest()
>>> req.prepare(method='post',url='http://localhost',data={"key":"val"},params={"query":"hogehoge"})
>>> del req.headers['Content-Type']
>>> with Session() as sess:
... res = sess.send(req)
...
>>>
4行目のreq.prepare
を実行した段階で内部的にはContent-Type
がセットされているので、5行目でヘッダを削除します。このコードを実行しながらtcpdumpでキャプチャ結果は以下のようになりました。
POST /?query=hogehoge HTTP/1.1
Host: localhost
Accept-Encoding: identity
Content-Length: 7
key=val
Content-Type
なしでPOSTできていることが分かります。
まとめ
今回紹介したようなケースは稀だとは思いますが、高レベルなAPIで望み通りのHTTPヘッダが生成されない場合の対応として、低レベルなAPIについても利用方法をしっかりと抑えておきたいですね。