【小ネタ】PythonのライブラリRequestsからPOSTリクエストを発行する際にリクエストヘッダからContent-Typeを削ってみた

一風変わった要件に対応してみました
2021.03.31

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() を介して直接本文を読み取ると、このメソッドの実行が妨げられる可能性があります。

インターフェース ServletRequest

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無しのリクエストを生成して対応することになりました。

RFC7231

環境

今回使用した各バージョンは以下の通りです

  • 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についても利用方法をしっかりと抑えておきたいですね。