Python標準のHTTPサーバーをカスタマイズしたい時
5/11/2022

pythonのライブラリのテストを書くときに、簡易的なサーバーを立てたい時があり、http.serverライブラリを漁ったのでメモ

pythonの標準ライブラリであるhttpには、簡易HTTPサーバがついている。 機能は非常にシンプルなもので、GETとHEADリクエストにしか対応しておらず、リクエストされたファイルがサーバ上にあるかを確認し、あれば返すというものです。 実際の公式ドキュメントのサンプルコード[1]はこんな感じ

import http.server
import socketserver

PORT = 8000

Handler = http.server.SimpleHTTPRequestHandler

with socketserver.TCPServer(("", PORT), Handler) as httpd:
    print("serving at port", PORT)
    httpd.serve_forever()

これだけで簡易サーバが動く。 (ちなみに、CGIHTTPRequestHandlerを使うのであればPOSTも捌ける)

しかし、単にサーバ上のファイルを返すだけでなく、レスポンスをカスタマイズしたい!という時どうすれば良いか。

結論はSimpleHTTPRequestHandlerやその上位クラスを継承してカスタマイズハンドラーを作るわけだが、わかりづらいしカスタマイズして使うことを推奨するような設計になっていないと思う。

というわけで、今回このAPIクライアントのテストを作るにあたり、ここらへんをいじったので、メモしておく

全体的な処理の流れは

serve_forever()でlisten ↓ リクエストが来る ↓ HTTPメソッドに対応したメソッドがよばれる ↓ そこのメソッドでレスポンスを返す

まぁなんの変哲もないという感じだけど、一応。

ではここで具体例を書いてみる。

ここでは、どんなパスでリクエストが飛んできてもルートのレスポンスを返す「RedirectRoootHTTPRequestHandler」を定義してみる。

class RedirectRoootHTTPRequestHandler(SimpleHTTPRequestHandler):
    def __init__(self, *args, directory=None, **kwargs):
        super().__init__(*args, directory=directory, **kwargs)

    def do_GET(self):
        self.path = "/"
        f = self.send_head()
        if f:
            try:
                self.copyfile(f, self.wfile)
            finally:
                f.close()

前に「HTTPメソッドに対応したメソッドがよばれる」と書いたが、GETに対応したメソッドがまさに「do_GET」だ。 面白いことに(?)、この対応したメソッドを呼ぶ処理は、二つ上の継承元である「BaseHTTPRequestHandler」で記述されているのだが、

mname = 'do_' + self.command
if not hasattr(self, mname):
    self.send_error(
        HTTPStatus.NOT_IMPLEMENTED,
        "Unsupported method (%r)" % self.command)
    return
method = getattr(self, mname)
method()

と、"do_" + HTTPメソッドでメソッドを探して対応するメソッドを呼び出している。

そして肝心のリダイレクトするような処理は

self.path = "/"

self.pathには元々リクエストされたパスが格納されており、copyfile(f, self.wfile) の中で参照している。

ちなみに、self.wfile はレスポンスを書き込む方のファイルオブジェクトで、f はレスポンスとなるファイルオブジェクト。

self.send_head() では、レスポンスヘッドを送りつつ、リクエストされたパスのファイルをopen()している。

ちなみに、パスがディレクトリとなっていた場合は、ちゃんとそのディレクトリのファイル一覧が返される。

  • command: リクエストHTTPメソッド
  • path : リクエスト対象のパス
  • version: HTTPバージョン
  • client_address: そのまま。クライアントアドレス

結構前(2021-09-21)に書いてた記事が途中で下書きのまま終わっているのを発見しました! 途中ながらとりあえず公開します! あんま需要なさそうだし!

1: https://docs.python.org/ja/3/library/http.server.html