Rack API is awkward
Mar 3, 2011 @ 12:24 pmTL;DR: Rack API is poor when you consider streaming response bodies.
class FooApplication
class ErbPage
def to_a
head = "the head tag"
sleep(2)
body = "the body tag"
sleep(2)
[head, body]
end
end
def call(env)
[200, {}, ErbPage.new.to_a]
end
end
class FakeRack
def serve(application)
status, headers, body = application.call({})
p :status => status
p :headers => headers
body.each do |string|
p string
end
body.close if body.respond_to?(:close)
end
end
app = FooApplication.new
rack = FakeRack.new
rack.serve app
$ time ruby foo.rb
{:status=>200}
{:headers=>{}}
"the head tag"
"the body tag"
real 0m4.008s
user 0m0.003s
sys 0m0.003s
class ResponseTimer
def initialize(app)
@app = app
end
def call(env)
now = Time.now
status, headers, body = @app.call(env)
headers['X-Response-Took'] = Time.now - now
[status, headers, body]
end
end
app = FooApplication.new
timer = ResponseTimer.new app
rack = FakeRack.new
rack.serve timer
$ time ruby foo.rb
{:status=>200}
{:headers=>{"X-Response-Took"=>3.999937}}
"the head tag"
"the body tag"
real 0m4.010s
user 0m0.004s
sys 0m0.004s
class FooApplication
class ErbPage
def each
head = "the head tag"
yield head
sleep(2)
body = "the body tag"
yield body
sleep(2)
end
end
def call(env)
[200, {}, ErbPage.new]
end
end
$ time ruby foo.rb
{:status=>200}
{:headers=>{"X-Response-Took"=>1.1e-05}}
"the head tag"
"the body tag"
real 0m4.032s
user 0m0.027s
sys 0m0.016s
class ResponseTimer
def initialize(app)
@app = app
end
def call(env)
now = Time.now
status, headers, body = @app.call(env)
newbody = []
body.each { |str| newbody << str }
headers['X-Response-Took'] = Time.now - now
[status, headers, newbody]
end
end
class ResponseTimer
class TimerProxy
def initialize(body)
@now = Time.now
@body = body
end
def close
@body.close if @body.respond_to?(:close)
$stderr.puts({'X-Response-Took' => (Time.now - @now)})
end
def each(&block)
@body.each(&block)
end
end
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
[status, headers, TimerProxy.new(body)]
end
end
$ time ruby foo.rb
{:status=>200}
{:headers=>{}}
"the head tag"
"the body tag"
{"X-Response-Took"=>4.000268}
real 0m4.044s
user 0m0.029s
sys 0m0.015s
class ResponseTimer
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body.extend(Module.new {
now = Time.now
define_method(:close) do
super if defined?(super)
$stderr.puts({'X-Response-Took' => (Time.now - now)})
end
})
[status, headers, body]
end
end
class EndOfLife
attr_reader :callbacks
def initialize(app)
@app = app
@callbacks = []
end
def call(env)
status, headers, body = @app.call(env)
body.extend(Module.new {
attr_accessor :eol
def close
super if defined?(super)
eol.callbacks.each { |cb| cb.call }
end
})
body.eol = self
[status, headers, body]
end
end
app = FooApplication.new
eol = EndOfLife.new app
eol.callbacks << lambda { puts "it finished!" }
rack = FakeRack.new
rack.serve eol
Edit: I just noticed that Rack contains a “timer” middleware similar to the one I’ve implemented in this blog post. You can view the broken middleware here.