More than 1 year has passed since last update.

Rack や WSGI の代わりになる仕様を考えてみました (ライブラリ (rack.rb や wsgiref.py) のほうではなく、プロトコル仕様のほうです)。自分のアイデアを書き連ねただけなので、まとまってないかもしれませんがご了承ください。

なお本稿は、今後何度か改訂すると思います。ご意見があればご自由にコメントしてください。

  • 【対象読者】Rack や WSGI に興味のある人
  • 【必要な知識】Rack や WSGI の基礎知識

Rack と WSGI の概要

Ruby の Rack や Python の WSGI は、HTTP のリクエストとレスポンスを抽象化した仕様です。

たとえば Rack では:

  • 引数として、リクエストを表す Hash オブジェクトを受け取り、
  • 戻り値として、レスポンスのステータスコードとヘッダーとボディを返します。
class RackApp
  def call(env)    # env はリクエストを表す Hash オブジェクト
    status  = 200                             # ステータスコード
    headers = {"Content-Type"=>"text/plain"}  # ヘッダー
    body    = "Hello"                         # ボディ
    return status, headers, [body]   # この 3 つがレスポンスを表す
  end
end

このように HTTP のリクエストとレスポンスを抽象化した仕様が、Ruby の Rack や Python の WSGI です。

これにより Web アプリケーションは、アプリケーションサーバ (WEBrick や Unicorn や Puma や UWSGI や waitress) が Rack や WSGI に対応していれば、どれでも使うことができます。たとえば開発中は手軽に使える WEBrick や waitress を使い、本番環境では高速な Unicorn や Puma や UWSGI を使う、という切り替えが簡単にできます。

また Rack や WSGI は、いわゆるデコレータパターンを使うことで、簡単に機能を追加できるように設計されています。たとえば、

  • セッション機能を追加する
  • リクエスト処理にかかった時間を記録する
  • エラーページの表示を開発環境と本番環境で切り替える

といったことが、Web アプリケーションの変更なしに実現できます。

## オリジナルの Rack アプリケーション
app = RackApp()

## たとえば、セッション機能を追加する
require 'rack/sesison/cookie'
app = Rack::Session::Cookie.new(app,
        :key => 'rack.session', :path=>'/',
        :expire_after => 3600,
        :secret => '54vYjDUSB0z7NO0ck8ZeylJN0rAX3C')

## たとえば、開発環境のときだけ詳細なエラーを表示する
if ENV['RACK_ENV'] == "development"
  require 'rack/showexceptions'
  app = Rack::ShowExceptions(app)
end

このように、もとの Web アプリケーションに機能を追加するためのラッパーオブジェクトを、Rack や WSGI では「ミドルウェア (Middleware)」といいます。上の例だと、Rack::Session::CookieRack::ShowException がミドルウェアです。

WSGI (Python) の問題点

WSGI は、Rack のもとになった仕様です。WSGI がなければ Rack は生まれなかったでしょう。

WSGI が登場した時期は、似たようなものとして Java の Servlet がありました。しかし Servlet の仕様はかなり複雑であり、実装するのが大変でした1
また仕様が複雑なせいでアプリケーションサーバごとに挙動が微妙に異なることもあり、結局のところみんな仕様書を見ずに、リファレンス実装である Tomcat を動かしてみて仕様を確認するという状態でした。

そういう状態だったので、Servlet の理念には共感するものの、仕様はまったく異なるとてもシンプルなものとして WSGI は登場しました。

具体的なコードをみてみましょう。以下は WSGI のサンプルコードです。

class WSGIApp(object):

  ## environ はリクエストを表すハッシュ (辞書) オブジェクト
  def __call__(self, environ, start_response):
    status  = "200 OK"      # 数値ではなく文字列
    headers = [             # ハッシュではなくキーと値のリスト
      ('Content-Type', 'text/plain'),
    ]
    start_response(status, headers)   # レスポンスを開始する
    return [b"Hello World"]  # ボディを返す

これを見ると、Rack と結構違うことが分かります。

  • レスポンスのステータスは、Rack では数値 (ex: 200) ですが、WSGI では文字列 (ex: "200 OK") です。 これは独自のステータスコードを使う場合に違いとなります。 たとえば「509 Bandwidth Limit Exceeded」という独自ステータスコードを使いたい場合、WSGI では何の問題もありませんが、Rack では「509」は簡単に指定できても「Bandwidth Limit Exceeded」を指定する方法が (仕様上は) ありません。
  • レスポンスのヘッダーは、Rack ではハッシュオブジェクトですが、WSGI ではキーと値のリストです。 これは、レスポンスヘッダーでは Set-Cookie ヘッダーが複数個登場する可能性があるからです。 Set-Cookie ヘッダーが複数ある場合、WSGI ではそれを自然に表現できますが、Rack だと値を複数行で表す必要があります。
  • WSGI では、レスポンスの開始時に呼ばれるコールバック関数が必要です。これは Rack と比べると初心者にはわかりにくいし、使うのも少し面倒です。
  • WSGI での戻り値は、レスポンスボディだけです。Rack では、ステータスコードとヘッダーとボディの 3 つを返します。
  • WSGI におけるリクエストヘッダーとレスポンスヘッダーは、キーも値も str (つまり Python2 ならバイナリ、Python3 ならユニコード文字列) です。ただしレスポンスボディは、必ずバイナリ (のリスト) です。

さて個人的な意見ですが、WSGI の最大の問題点は、start_response() というコールバック関数の存在でしょう。これがあるせいで、初心者が WSGI を理解するにはまず「関数を受けとる関数 (高階関数)」を理解しなければならず、敷居が高いです2

また WSGI アプリケーションを呼び出すのも、start_response() のせいで無駄に手間がかかります。これ、ほんと面倒。

## いちいちこういうのを用意しないと、
class StartResponse(object):
  def __call__(self, status, headers):
    self.status = status
    self.headers = headers

## WSGIアプリケーションを呼び出せない
app = WSGIApplication()
environ = {'REQUEST_METHOD': 'GET', ...(snip)... }
start_response = StartResponse()
body = app.__call__(environ, start_response)
print(start_response.status)
print(start_response.headers)

(実は WSGI (PEP-333) に対し、この点を改善した Web3 (PEP-444) という仕様が過去に提案されていました。この Web3 ではコールバック関数を廃し、Rack と同じように status, headers, body を返す仕様になっていました。個人的に期待してたんですが、結局は採用されませんでした。残念です。)

また WSGI では、レスポンスヘッダーがハッシュ (辞書) オブジェクトではなく、キーと値のリストになっているのもちょっと困ります。なぜなら、ヘッダーを設定するときにいちいちリストを検索しなければならないからです。

## たとえばこんなレスポンスヘッダーがあるとして、
resp_headers = [
  ('Content-Type', "text/html"),
  ('Content-Disposition', "attachment;filename=index.html"),
  ('Content-Encoding', "gzip"),
]
## 値を設定するにはいちいちリストを検索する必要がある
key = 'Content-Length'
val = str(len(content))
for i, (k, v) in enumerate(resp_headers):
  if k == key:   # or k.tolower() == key.tolower()
    break
else:
  i = -1
if i >= 0:   # あれば上書き
  resp_headers[i] = (key, val)
else:        # なければ追加
  resp_headers.append((key, val))

これは面倒くさい。専用のユーティリティ関数を定義するのもいいですけど、どうせならハッシュ (辞書) オブジェクトを使ったほうがよかったです。

## ハッシュオブジェクト (辞書オブジェクト) なら…
resp_headers = {
  'Content-Type':        "text/html",
  'Content-Disposition': "attachment;filename=index.html",
  'Content-Encoding':    "gzip",
]
## 値を設定するのがとても簡単!
## (ただしキー名の大文字小文字が統一されていることが前提)
resp_headers['Content-Length'] = str(len(content))

Rack (Ruby) の問題点

Rack (Ruby) は、WSGI (Python) を参考にして決められた仕様です。Rack は WSGI ととてもよく似ていますが、よりシンプルになるように改良されています。

class RackApp
  def call(env)   # env はリクエストを表すハッシュオブジェクト
    status  = 200
    headers = {
      'Content-Type' => 'text/plain;charset=utf-8',
    }
    body    = "Hello World"
    return status, headers, [body]  # この3つがレスポンスを表す
  end
end

具体的な違いは次の通りです。

  • WSGI と比べると、Rack ではコールバック関数を使っていません。 そのおかげで、「リクエストを受け取りレスポンスを返す」という動作が、とても自然に表現されてます。 これなら初心者でも理解できるでしょう。
  • Rack では、レスポンスヘッダーはハッシュオブジェクトを使います。 そのため、値の設定がとても簡単にできます (ただしキーがたとえば 'Content-Type' だったり 'content-type' だったりまちまちだと問題になる可能性があるので、統一しておく必要があります)。
  • Rack では、ステータスコードは数値で表します。カスタムステータスコードを指定した場合の挙動は仕様外であり、各アプリケーションサーバごとに設定が必要となるでしょう。

さて、Rack ではレスポンスヘッダーをハッシュオブジェクトで表します。この場合、Set-Cookie のように複数回登場する可能性があるヘッダーはどうしたらいいでしょうか。

Rack の仕様では、次のような記述があります。

The values of the header must be Strings, consisting of lines (for multiple header values, e.g. multiple Set-Cookie values) separated by "\n".

つまりヘッダーの値が複数行の文字列なら、ヘッダーが複数回登場したとみなします。

しかしこの仕様はどうかと思います。なぜならすべてのレスポンスヘッダーに対して、改行文字を含むかどうかを調べる必要があるからです。これはパフォーマンスを落とします。

headers.each do |k, v|
  v.split(/\n/).each do |s|   # ←二重ループ ;-(
    puts "#{k}: #{s}"
  end
end

これよりは、「複数回登場するヘッダーは値を配列にする」という仕様のほうがよさそうです。

headers.each do |k, v|
  if v.is_a?(Array)     # ←こっちのほうがマシ
    v.each {|s| puts "#{k}: #{s}" }
  else
    puts "#{k}: #{v}"
  end
end

あるいは、Set-Cookie ヘッダーだけ特別扱いするのでもいいです。複数回登場する可能性のあるヘッダーは Set-Cookie ぐらいでしょうから3、この仕様でも悪くないです。

set_cookie = "Set-Cookie"
headers.each do |k, v|
  if k == set_cookie     # ← Set-Cookieだけ特別扱い
    v.split(/\n/).each {|s| puts "#{k}: #{s}" }
  else
    puts "#{k}: #{v}"
  end
end

もう一点、レスポンスボディの close() メソッドについて。
Rack や WSGI の仕様では、レスポンスボディのオブジェクトが close() というメソッドを持っている場合、クライアントへのレスポンスが完了したらアプリケーションサーバが close() を呼び出すという仕様になっています。これは、主にレスポンスボディが File オブジェクトの場合を想定した仕様です。

  def call(env)
    filename = "logo.png"
    headers = {'Content-Type'   => "image/png",
               'Content-Length' => File.size(filename).to_s}
    ## ファイルを open する
    body = File.open(filename, 'rb')
    ## open したファイルは、レスポンス完了時にはアプリサーバによって
    ## 自動的に close() が呼ばれる
    return [200, headers, body]
  end

けどこれは、each() メソッドの終わりでファイルを close すればいいだけのように思います。

class AutoClose
  def initialize(file)
    @file = file
  end
  def each
    ## これは行単位での読み出しなので効率はよくない
    #@file.each |line|
    #  yield line
    #end
    ## もっと大きいサイズで読み出したほうが効率はいい
    while (s = @file.read(8192))
      yield s
    end
  ensure            # ファイルを全部読み出したら or エラーがあったら
    @file.close()   # 自動的に close する
  end
end

この、close() メソッドがあれば呼び出すという仕様は、レスポンスボディの each() メソッドが一回も呼ばれないケースでは必要なんでしょう。個人的には、こんな「File オブジェクトのことしか考えてません」的な仕様よりは、xUnit における teardown() のような後始末用の仕様をきちんと考えるべきだったと思います (とはいえ妙案があるわけでもないのですが)。

Environment オブジェクトについて

Rack でも WSGI でも、HTTP リクエストはハッシュ (辞書) オブジェクトとして表されます。これは Rack や WSGI の仕様では Environment と呼ばれています。

これがどんなものか、表示してみましょう。

## Filename: sample1.ru

require 'rack'

class SampleApp
  ## Inspect Environment data
  def call(env)
    status = 200
    headers = {'Content-Type' => "text/plain;charset=utf-8"}
    body = env.map {|k, v| "%-25s: %s\n" % [k.inspect, v.inspect] }.join()
    return status, headers, [body]
  end
end

app = SampleApp.new

run app

これを rackup sample1.ru -E production -s puma -p 9292 で実行し、ブラウザで http://localhost:9292/index?x=1 にアクセスすると、たとえばこんな結果になりました。これが Environment の中身です。

"rack.version"           : [1, 3]
"rack.errors"            : #<IO:<STDERR>>
"rack.multithread"       : true
"rack.multiprocess"      : false
"rack.run_once"          : false
"SCRIPT_NAME"            : ""
"QUERY_STRING"           : "x=1"
"SERVER_PROTOCOL"        : "HTTP/1.1"
"SERVER_SOFTWARE"        : "2.15.3"
"GATEWAY_INTERFACE"      : "CGI/1.2"
"REQUEST_METHOD"         : "GET"
"REQUEST_PATH"           : "/index"
"REQUEST_URI"            : "/index?x=1"
"HTTP_VERSION"           : "HTTP/1.1"
"HTTP_HOST"              : "localhost:9292"
"HTTP_CACHE_CONTROL"     : "max-age=0"
"HTTP_COOKIE"            : "_ga=GA1.1.1305719166.1445760613"
"HTTP_CONNECTION"        : "keep-alive"
"HTTP_ACCEPT"            : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
"HTTP_USER_AGENT"        : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9"
"HTTP_ACCEPT_LANGUAGE"   : "ja-jp"
"HTTP_ACCEPT_ENCODING"   : "gzip, deflate"
"HTTP_DNT"               : "1"
"SERVER_NAME"            : "localhost"
"SERVER_PORT"            : "9292"
"PATH_INFO"              : "/index"
"REMOTE_ADDR"            : "::1"
"puma.socket"            : #<TCPSocket:fd 14>
"rack.hijack?"           : true
"rack.hijack"            : #<Puma::Client:0x3fd60649ac48 @ready=true>
"rack.input"             : #<Puma::NullIO:0x007fac0c896060>
"rack.url_scheme"        : "http"
"rack.after_reply"       : []

(rack.hijack は Rack 1.5 から導入された新機能です。詳しくはこちらをご覧ください。)

この Environment には、大きく分けると 3 種類のデータが入ってます。

  • HTTP リクエストヘッダー
    • キーが「HTTP_」で始まるもの (ただし HTTP_VERSION は除く)
    • (この例にはないが) CONTENT_TYPE と CONTENT_LENGTH
  • ヘッダー以外のリクエスト情報
    • REQEUST_METHOD
    • PATH_INFO
    • QUERY_STRING
    • HTTP_VERSION
    • REMOTE_ADDR
    • rack.input
    • rack.url_scheme
    • puma.socket
  • アプリケーションサーバからの情報
    • SERVER_NAME
    • SERVER_PORT
    • rack.version などキーが「rack.」で始まるもの

これらがごっちゃになって格納されているのが Environment です。個人的にはこのような仕様が好きではなく、せめてリクエストヘッダーとそれ以外ぐらいは分けてほしいと思います。

なぜこんな仕様になっているかというと、CGI の仕様をもとにしているからです。今の若い人は CGI なんか知らないと思いますが、そういうのがあって、昔はとてもよく使われてたんですよ。この CGI の仕様を借りて WSGI が Environment の仕様を決め、Rack もそれを継承しています。そのため、CGI を知らない人から見ると奇妙な仕様に見えるでしょう。「なんで User-Agent ヘッダーが HTTP_USER_AGENT に変わってるの?そのまま User-Agent 文字列を使えばいいのに。」のような感想が出てきそうです。

Environment オブジェクトの問題点

すでに見たように、Environment オブジェクトは要素を何十個も含んだハッシュオブジェクトです。

パフォーマンスの観点から言うと、要素が何十個もあるようなハッシュオブジェクトの生成は、Ruby や Python では動作コストがかなりかかるため、望ましくありません。たとえば Ruby on Rails より 100 倍速いフレームワークである Keight.rb の場合、リクエストの処理にかかる時間よりも、Environment オブジェクトを生成するほうに時間がかかる場合があります

実際に、ベンチマークスクリプトで確かめてみましょう。

# -*- coding: utf-8 -*-
require 'rack'
require 'keight'
require 'benchmark/ips'

## アクションクラス (MVC でいうところのコントローラ) を作成
class API < K8::Action
  mapping '/hello',  :GET=>:say_hello
  def say_hello()
    return "<h1>Hello, World!</h1>"
  end
end

## Rack アプリケーションを作成し、アクションクラスを割り当てる
mapping = [
    ['/api',   API],
]
rack_app = K8::RackApplication.new(mapping)

## 実行例
expected = [
  200,
  {"Content-Length"=>"22", "Content-Type"=>"text/html; charset=utf-8"},
  ["<h1>Hello, World!</h1>"]
]
actual = rack_app.call(Rack::MockRequest.env_for("/api/hello"))
actual == expected  or raise "assertion failed"

## GET /api/hello を表す Environemnt オブジェクト
env = Rack::MockRequest.env_for("/api/hello")

## ベンチマーク
Benchmark.ips do |x|
  x.config(:time => 5, :warmup => 1)

  ## Environment オブジェクトを新しく生成する (コピーする)
  x.report("just copy env") do |n|
    i = 0
    while (i += 1) <= n
      env.dup()
    end
  end

  ## Environment オブジェクトを生成して、リクエストを処理する
  x.report("Keight (copy env)") do |n|
    i = 0
    while (i += 1) <= n
      actual = rack_app.call(env.dup)
    end
    actual == expected  or raise "assertion failed"
  end

  ## Environment オブジェクトを再利用して、リクエストを処理する
  x.report("Keight (reuse env)") do |n|
    i = 0
    while (i += 1) <= n
      actual = rack_app.call(env)
    end
    actual == expected  or raise "assertion failed"
  end

  x.compare!
end

これを実行すると、たとえば次のような結果が得られました (Ruby 2.3, Keight.rb 0.2, OSX El Capitan)。

Calculating -------------------------------------
       just copy env    12.910k i/100ms
   Keight (copy env)     5.523k i/100ms
  Keight (reuse env)    12.390k i/100ms
-------------------------------------------------
       just copy env    147.818k (± 8.0%) i/s -    735.870k
   Keight (copy env)     76.103k (± 4.4%) i/s -    381.087k
  Keight (reuse env)    183.065k (± 4.8%) i/s -    916.860k

Comparison:
  Keight (reuse env):   183064.5 i/s
       just copy env:   147818.2 i/s - 1.24x slower
   Keight (copy env):    76102.8 i/s - 2.41x slower

最後の 3 行から次のことがわかります。

  • Environment オブジェクトを生成せず再利用すれば 18.3 万 i/s なのに、リクエストごとに生成すると半分以下の 7.6 万 i/s になった。
  • 再利用してリクエストを処理した場合 (18.3 万 i/s) よりも、リクエストを処理せずコピーするだけの場合 (14.8 万 i/s) のほうが遅い。つまりリクエストの処理にかかる時間よりも、Environment オブジェクトを生成するだけのほうがコストが高い。

このような状況なので、これ以上フレームワークを高速化してもアプリケーションは大して速くはならないでしょう。この行き詰まった状況を打破するには、Rack の仕様自体を改善するのがよさそうです。

デコレータパターンの問題点

(TODO)

次世代の Rack や WSGI を考えてみる

さて、ようやく本題に入ります。

今まで説明したような問題点を解消するために、現行の Rack や WSGI にとってかわるものを考えてみました。いわゆる、「ぼくのかんがえたさいきょうのらっく」ですね。

新しい仕様でも、HTTP リクエストとレスポンスを抽象化することには変わりません。なので、この 2 つをどう抽象化するかを中心に説明します。

また現在の Rack や WSGI では CGI の仕様を部分的に継承しています。しかし CGI は環境変数経由でデータを渡すことを想定した、古い時代の仕様です。今の時代にはふさわしくありませんので、CGI の仕様は忘れていいでしょう。

HTTP リクエスト

HTTP リクエストは、以下のような要素に分けられます。

  • リクエストメソッド (GET/POST/PUT/DELETE/PATCH/HEADER/OPTIONS/TRACE)
  • リクエストパス (ex: '/index.html')
  • リクエストヘッダー (ex: {'Host'=>..., 'User-Agent'=>...})
  • クエリパラメータ (ex: '?x=1')
  • I/O (rack.input, rack.errors, rack.hijack or puma.socket)
  • その他リクエスト情報 (HTTP_VERSION, REMOTE_ADDR, rack.url_scheme)
  • サーバー情報 (SERVER_NAME, SERVER_PORT, rack.version)

リクエストメソッドは、大文字の文字列か Symbol でいいでしょう。性能を考えると Symbol のほうがよさそうです。

meth = :GET

リクエストパスは、文字列でいいでしょう。Rack では PATH_INFO だけでなく SCRIPT_NAME も考慮する必要がありますが、いまや SCRIPT_NAME を使う人もいないでしょうから、PATH_INFO 相当だけを考えることにします。

path = "/index.html"

リクエストヘッダーは、ハッシュオブジェクトでいいでしょう。また User-Agent → HTTP_USER_AGENT のような変換はしたくないですが、HTTP/2 ではヘッダー名が小文字らしいので、それに合わせることになるでしょう。

headers = {
  "host"       => "www.example.com",
  "user-agent" => "Mozilla/5.0 ....(snip)....",
  ....(snip)....,
}

クエリパラメータは、nil か、または文字列です。? がなければ nil になって、あれば文字列になります (空文字の可能性もあります)。

query = "x=1"

I/O 関連 (rack.input と rack.errors と rack.hijack or puma.socket) は、1 つの配列にすればよさそうです。これらはちょうど、stdin と stderr と stdout に相当する・・・んじゃないかな?もしかしたら socket は rack.input を兼ねるかもしれないけど、よく知らないのでここでは分けておきます。

ios = [
  StringIO.new(),   # rack.input
  $stderr,          # rack.errors
  puma_socket,
]

その他リクエスト情報は、リクエストごとに値が変わります。これはハッシュオブジェクトにすればいいでしょう。

options = {
  http:       "1.1",    # HTTP_VERSION
  client:     "::1",    # REMOTE_ADDR
  protocol:   "http",   # rack.url_scheme
}

最後のサーバ情報は、アプリケーションサーバに変更がない限りは値が変わらないはずです。だから一度ハッシュオブジェクトとして作れば、使い回しができます。

server = {
  name:  "localhost".freeze,    # SERVER_NAME
  port:  "9292".freeze,         # SERVER_PORT
  'rack.version':       [1, 3].freeze,
  'rack.multithread':   true,
  'rack.multiprocess':  false,
  'rack.run_once':      false,
}.freeze

これらを受けとるような Rack アプリケーションを考えてみましょう。

class RackApp
  def call(meth, path, headers, query, ios, options, server)
    input, errors, socket = ios
    ...
  end
end

うわー、引数が 7 個もあるわー。
これはちょっとイケてないですね。最初の 3 つ (meth と path と headers) はリクエストのコアというべき部分なので単独の引数のままにしておくとして、query と ios は options にまとめられそうです。

options = {
  query:    "x=1",     # QUERY_STRING
  #
  input:    StringIO.new,   # rack.input,
  error:    $stderr,        # rack.erros,
  socket:   puma_socket,    # rack.hijack or puma.socket
  #
  http:     "1.1",     # HTTP_VERSION
  client:   "::1",     # REMOTE_ADDR
  protocol: "http",    # rack.url_scheme
}

こうすると、引数が 7 つから 5 つに減ります。

class RackApp
  def call(meth, path, headers, options, server)
    query  = options[:query]
    input  = options[:input]
    error  = options[:error]
    socket = options[:socket]   # or :output ?
    ...
  end
end

まあ、これなら使ってもいいかなと思えます。

HTTP レスポンス

HTTP レスポンスは従来通り、ステータスとヘッダーとボディの 3 つで表せばいいでしょう。

  def call(meth, path, headers, options, server)
    status  = 200
    headers = {"content-type"=>"application/json"},
    body    = '{"message":"Hello!"}'
    return status, headers, body
  end

ただ、Content-Type ヘッダーは特別扱いしてもいいかなと思います。なぜなら現在の Rack アプリケーションでは、ヘッダとして {"Content-Type"=>"text/html}"{"Content-Type"=>"application/json"} のように、Content-Type しか含まないケースが多いからです。そのため、Content-Type だけ特別扱いして独立させると、少し簡潔になります。

  def call(meth, path, headers, options, server)
    ## これよりも
    return 200, {"Content-Type"=>"text/plain"}, ["Hello"]
    ## こっちのほうが簡潔
    return 200, "text/plain", {}, ["Hello"]
  end

ほかにも、いくつか論点があります。


ステータスは整数か文字列か?

整数でいいと思いますが、カスタムステータスを指定する方法があったほうがいいでしょう。ただそれは Rack の仕様ではなく、各アプリケーションサーバごとに登録する方法があればそれでいいと思います。

ヘッダーはハッシュかリストか?

これはもうハッシュでいいでしょう。

Set-Cookie ヘッダーが複数個ある場合はどうするか?

これはすでに説明したように、ヘッダーの値に文字列の配列を許せばいいでしょう。そしてヘッダー値は改行文字を含んではならないと決めましょう。

ボディに文字列を許すかどうか?

現行の Rack の仕様では、ボディは each() メソッドを実装していなければならないため、ボディに文字列を直接指定できません。かわりに、文字列の配列を指定するのが定番です。

しかしほとんどのレスポンスがボディとして文字列を返すのだから、いちいち配列で包むのは無駄です。できれば、ボディは「文字列、または each() で文字列を返すオブジェクト」とするのがいいでしょう。


レスポンス完了時にボディの close() メソッドを呼び出すべきか?

これは難しい問題です。すでに説明したように、each() メソッドが必ず呼び出されることが保証されるなら、この仕様はなくても構わないです。ただそういう保証はないので、かわりに close() を呼び出すことを保証しているのでしょう。

しかし本当に望ましいのは、teardown() 相当の機能を用意することでしょう。具体的な仕様が思いつかないのが残念です4

デコレータパターン

(TODO)

イベント駆動や non-blocking I/O

(TODO)

HTTP/2 対応

(TODO)

おわりに

識者のご意見をいただきたい。

参考文献

WSGI 関連

Rack 関連

関連リンク

ちょうど Rack のメーリングリストに HTTP2 対応についての質問が出てた。関連して Rack2 の話題が少し出たので、いろいろぐぐってみた。


  1. 物事を必要以上に複雑にするのは、Java と IBM の得意技です。 

  2. 「高階関数なんて簡単に理解できるだろ」とおっしゃる上級者の方は、初心者がどこでつまづくかを理解する才能が根本的に欠けてるので、初心者の相手はせずに関数型言語の世界にでもお帰りください。名選手、名監督にあらず。スポーツ万能な人は運動オンチの指導には向かない。 

  3. ほかに Via ヘッダーがあったと思うけど、Rack や WSGI の範疇では扱わないから Set-Cooki だけ考慮すればいいはず。 

  4. rack.after_reply というのがそれかなと思いましたが、どうも Puma の独自機能のようです。