何となくの知識しか持っていないHTTPの動作を理解するために、Gaucheのrfc.httpモジュールを使わずHTTPクライアント用の低レベルライブラリを書いてみることにした。rfc.httpモジュールを使わないのは持続的接続(Keep Alive)の動作を知りたいからという理由が大きい。
プログラムを書いていくと、どうも持続的接続とリダイレクトの扱いが面倒な感じがする。これらがなければ、ソケットを開く、データを送信する、データを受信する、ソケットを閉じる、という動作で完結するので、書く手間はあってもプログラムはそれほど入り組まないと思えるのだけど、持続的接続とリダイレクトがからむと途端に面倒になる感じがする。「使いやすいAPIデザイン」に
あまり典型的じゃない望み
- インクリメンタルな処理
- パーシステント接続
使いにくくする もしくは使えなくする
とあってこれは一つの見識だと思うけど、そこに当てはまらない、APIを汚くするかもしれない処理を扱いたい場合どうすれば良いのだろう。
リダイレクトを自動で処理させようとすると、リダイレクト前のヘッダを捨てずに取っておきたい場合があったり、メソッドがPOSTだった場合リダイレクト後はGETに置き換えたり(この動作はRFC的には間違っているけど)する必要がある。こういう動作が必要になるのは例でいうと
- (HTTPSで暗号化した状態で)POSTでパスワードを送る。
- サーバは
- セッションIDをクッキーで送ってくる。
- リダイレクトさせるためにステータスコード302 Foundを返す(本当はこの場合リダイレクトさせるなら303 See Otherを使うべきだけど)。
このとき、リダイレクト前のヘッダ内容(クッキー)は捨てずに取っておく必要があるし、リダイレクト後のURIも覚えておく必要がある。またリダイレクト先にPOSTしてしまうとパスワードを暗号化されていない状態で(もしかしたら関係ないサーバーに)送ることになる。
さらに持続的接続を扱うと話がややこしくなる。ソケットを開いたり使いまわしたりをどう扱うものなのかよく判らない。特にリダイレクトする場合、リダイレクト前のソケットはどう扱うものなのか。
テストの書き方
これはどんなプログラムを書く場合にも言えることだけど、テストを書くのが難しい。テストを書くのは、作りたいプログラム自身を書くのと同じかそれ以上に難しいと思う。
プログラムの書き方については『プログラム書法』から様々なプログラミング言語本から色々なところに書かれているから多少は判る気がするけど、テストの書き方はそれに比べると全然判らない。「まずテストを書いて」というところで止まってしまう。
テスト用のHTTPサーバー
HTTPクライアントプログラムのテストをしようすると、比較的単純な動作をするHTTPサーバーが欲しくなる。それで「最小限の動作をするHTTPサーバを書くことにする」→「テストが必要なくらいには複雑になる」→「そのためのテストがいる」という結果になる気がする。
ゼロから書くとかなり大変そうなのでWEBrickで書いてみたけど、それでもテストが必要なくらいには複雑な気がする(WEBrickの使い方)。
http://localhost:8080/reflect/
で送ったデータをそのままレスポンスの本体にして返してくるし、
http://localhost:8080/content-length/ http://localhost:8080/chunked/ http://localhost:8080/closing/
でレスポンス本体の長さの表し方を変える。
http://localhost:8080/reflect/201/
などでステータスコードだけ変えることもできる。あと
X-Header-……
というヘッダーを送ると、X-Header-を取ったものをヘッダーに付け加えて送り返してくる。など。
これでもテスト用の機能としてまだかなり足りてないかも。
require 'webrick' # リクエストの生データを保存するための処理 module WEBrick class HTTPRequest alias :old_initialize :initialize def initialize(*args) @request_data = "" old_initialize(*args) end alias :old_read_data :_read_data def _read_data(*args) x = old_read_data(*args) # STDOUT.puts x # for debug @request_data << x x end def read_data(io, size) _read_data(io, :read, size) end def get_request_data body # リクエスト本体はbodyが呼ばれるまで読み込まれない @request_data end # 強制的にソケットを閉じ異常動作を発生させる def close_socket @socket.close end end end def start_test_server(config={}) default_config ={:BindAddress => '127.0.0.1', :Port => 8080} server = WEBrick::HTTPServer.new(default_config.merge config) server.mount_proc('/reflect') do |req, res| make_status(req, res) res.body << req.get_request_data end server.mount_proc('/content-length') do |req, res| make_status(req, res) make_body(req, res) end server.mount_proc('/chunked') do |req, res| make_status(req, res) make_body(req, res) res.chunked = true end server.mount_proc('/closing') do |req, res| make_status(req, res) make_body(req, res) res.chunked = false res.content_length = nil res.keep_alive = false end server.mount_proc('/found') do |req, res| res.status = 302 res["location"]= File.join('/reflect', req.path_info) end server.mount_proc('/looping') do |req, res| res.status = 302 res["location"]= File.join('/looping-2', req.path_info) end server.mount_proc('/looping-2') do |req, res| res.status = 302 res["location"]= File.join('/looping', req.path_info) end server.mount_proc('/endless') do |req, res| res.status = 302 res["location"]= File.join('/endless', rand(10000000).to_s) end # 返信せずソケットを閉じる。異常な動作 server.mount_proc('/error') do |req, res| req.close_socket end server.mount_proc('/exit') do |req, res| res.status = 200 server.shutdown end trap(:INT){server.shutdown} server.start end def make_status(req, res) if %r|/(\d\d\d)| =~ req.path_info res.status = $~[1].to_i end req.each{|key, val| if /^X-Header-([^:]*)/i =~ key res[$~[1]] = val end } end def make_body(req, res) if req.body res.body << req.body elsif %r|/\d\d\d/(.*)| =~ req.path_info res.body << $~[1] else res.body << req.path_info end end start_test_server()