7. Why it was made?
• Rack didn't have an API that allows
for IO-like streaming.
• for WebSocket
• for HTTP2
https://github.com/rack/rack/pull/
481#issue-9702395
7
8. Similar implementation
• Golang's Hijacker interface.
• Probably, This API would made
based on this interface.
https://github.com/rack/rack/pull/
481#issue-9702395
8
11. Two mode of Hijaking
• Full hijacking
• Partial hijacking
http://www.rubydoc.info/github/
rack/rack/master/file/
SPEC#Hijacking
11
12. The timing of Full
hijacking
• Request (before status)
12
13. The conditions of Full
hijacking
env['rack.hijack?'] == true
env['rack.hijack'].respond_to?(:call) == true
env['rack.hijack'].call must returns the io
env['rack.hijack'].call is assigned the io to
env['rack.hijack_io']
REQUIRED_METHOD =
[:read, :write, :read_nonblock, :write_nonblock, :fl
ush, :close, :close_read, :close_write, :closed?]
REQUIRED_METHOD.all? { |m|
env['rack.hijack_io'].respond_to?(m) } == true
13
14. Your responsibility of
Full hijacking
• Outputting any HTTP headers, if
applicable.
• Closing the IO object when you no
longer need it.
14
15. class HijackWrapper
include Assertion
extend Forwardable
REQUIRED_METHODS = [
:read, :write, :read_nonblock, :write_nonblock, :flush, :clos
e,
:close_read, :close_write, :closed?
]
def_delegators :@io, *REQUIRED_METHODS
def initialize(io)
@io = io
REQUIRED_METHODS.each do |meth|
assert("rack.hijack_io must respond to #{meth}")
{ io.respond_to? meth }
end
end
end
https://github.com/rack/rack/blob/
fd1fbab1ec8c7fc49ac805aac47b1f12d4cc5a99/lib/rack/lint.rb#L494-
L511
15
16. def check_hijack(env)
if env[RACK_IS_HIJACK]
original_hijack = env[RACK_HIJACK]
assert("rack.hijack must respond to call")
{ original_hijack.respond_to?(:call) }
env[RACK_HIJACK] = proc do
io = original_hijack.call
HijackWrapper.new(io)
env[RACK_HIJACK_IO] =
HijackWrapper.new(env[RACK_HIJACK_IO])
io
end
else
assert("rack.hijack? is false, but rack.hijack is
present") { env[RACK_HIJACK].nil? }
assert("rack.hijack? is false, but rack.hijack_io is
present") { env[RACK_HIJACK_IO].nil? }
end
end
https://github.com/rack/rack/blob/
fd1fbab1ec8c7fc49ac805aac47b1f12d4cc5a99/lib/rack/
lint.rb#L513-L562
16
17. The timing of Partial
hijacking
• Response (after headers)
17
18. The conditions of
Partial hijacking
• an application may set the special
header rack.hijack to an object
that responds to #call accepting
an argument that conforms to the
rack.hijack_io protocol.
18
20. def check_hijack_response(headers, env)
headers = Rack::Utils::HeaderHash.new(headers)
if env[RACK_IS_HIJACK] && headers[RACK_HIJACK]
assert('rack.hijack header must respond to #call') {
headers[RACK_HIJACK].respond_to? :call
}
original_hijack = headers[RACK_HIJACK]
headers[RACK_HIJACK] = proc do |io|
original_hijack.call HijackWrapper.new(io)
end
else
assert('rack.hijack header must not be present if server
does not support hijacking') {
headers[RACK_HIJACK].nil?
}
end
end
https://github.com/rack/rack/blob/
fd1fbab1ec8c7fc49ac805aac47b1f12d4cc5a99/lib/rack/
lint.rb#L564-L614
20
33. Rack::Handler::Webrick#serv
ice (Take out the io_lambda)
status, headers, body = @app.call(env)
begin
res.status = status.to_i
io_lambda = nil
headers.each { |k, vs|
if k == RACK_HIJACK
io_lambda = vs
elsif k.downcase == "set-cookie"
res.cookies.concat vs.split("n")
else
# Since WEBrick won't accept repeated headers,
# merge the values per RFC 1945 section 4.2.
res[k] = vs.split("n").join(", ")
end
}
https://github.com/rack/rack/blob/cabe6b33ca4601aa6acb56317ac1c819cf6dc4bb/
lib/rack/handler/webrick.rb#L86-L100
34. Rack::Handler::Webrick#serv
ice (Calls the io_lambda)
if io_lambda
rd, wr = IO.pipe
res.body = rd
res.chunked = true
io_lambda.call wr
elsif body.respond_to?(:to_path)
res.body = ::File.open(body.to_path, 'rb')
else
body.each { |part|
res.body << part
}
end
ensure
body.close if body.respond_to? :close
end
https://github.com/rack/rack/blob/cabe6b33ca4601aa6acb56317ac1c819cf6dc4bb/
lib/rack/handler/webrick.rb#L86-L100
35. response
<= Recv data, 35 bytes (0x23)
0000: David
0007: David
000e: David
0015: David
001c: David
== Info: transfer closed with outstanding read data
remaining
== Info: Curl_http_done: called premature == 1
== Info: Closing connection 0
https://gist.github.com/kysnm/
ca5237d4ac96764b9cfe6ac1547710cf
49. Puma::Client#call
# For the hijack protocol (allows us
to just put the Client object
# into the env)
def call
@hijacked = true
env[HIJACK_IO] ||= @io
end
https://github.com/puma/puma/blob/
3.6.1/lib/puma/client.rb#L69-L74
49
54. Puma::Server#handle_req
uest (response_hijack)
response_hijack = nil
headers.each do |k, vs|
case k.downcase
when CONTENT_LENGTH2
content_length = vs
next
when TRANSFER_ENCODING
allow_chunked = false
content_length = nil
when HIJACK
response_hijack = vs
next
end
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L653-
L666
54
65. Limitations
•I have not tried to spec out a full IO
API, and I'm not sure that we should.
•I have not tried to respec all of the
HTTP / anti-HTTP semantics.
•There is no spec for buffering or the
like.
•The intent is that this is an API to
"get out the way”.
https://github.com/rack/rack/pull/481
65
66. What?
this is a straw man that addresses this within
the confines of the rack 1.x spec. It's not an
attempt to build out what I hope a 2.0 spec
should be, but I am hoping that something like
this will be enough to aid Rails 4s ventures,
enable websockets, and a few other strategies.
With HTTP2 around the corner, we'll likely
want to revisit the IO API for 2.0, but we'll
see how this plays out. Maybe IO wrapped
around channels will be ok.
https://github.com/rack/rack/pull/481
66