Skip to content

Commit

Permalink
Add early hints feature
Browse files Browse the repository at this point in the history
This commit adds the early hints lambda to the headers hash. Users can
call it to emit the early hints headers. For example:

```
class Server
  def call env
    if env["REQUEST_PATH"] == "/"
      env['rack.early_hints'].call([{ link: "/style.css", as: "style" }, { link: "/script.js" }])
      [200, { "X-Hello" => "World" }, ["Hello world!"]]
    else
      [200, { "X-Hello" => "World" }, ["NEAT!"]]
    end
  end
end

run Server.new
```

In this example, the server sends stylesheet and javascript early hints
if the proxy supports it, it will send H2 pushes to the client.

Of course not every proxy server supports early hints, so to enable the
early hints feature with puma you have to pass the configuration variable,
`--early-hints`.
  • Loading branch information
eileencodes committed Sep 20, 2017
1 parent 176be58 commit 67b2347
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 1 deletion.
4 changes: 4 additions & 0 deletions lib/puma/cli.rb
Expand Up @@ -181,6 +181,10 @@ def setup_options
user_config.tcp_mode!
end

o.on "--early-hints", "Enable early hints support" do
user_config.early_hints!
end

o.on "-V", "--version", "Print the version information" do
puts "puma version #{Puma::Const::VERSION}"
exit 0
Expand Down
2 changes: 2 additions & 0 deletions lib/puma/const.rb
Expand Up @@ -221,5 +221,7 @@ module Const
HIJACK_P = "rack.hijack?".freeze
HIJACK = "rack.hijack".freeze
HIJACK_IO = "rack.hijack_io".freeze

EARLY_HINTS = "rack.early_hints".freeze
end
end
4 changes: 4 additions & 0 deletions lib/puma/dsl.rb
Expand Up @@ -241,6 +241,10 @@ def tcp_mode!
@options[:mode] = :tcp
end

def early_hints(answer=true)
@options[:early_hints] = answer
end

# Redirect STDOUT and STDERR to files specified.
def stdout_redirect(stdout=nil, stderr=nil, append=false)
@options[:redirect_stdout] = stdout
Expand Down
4 changes: 4 additions & 0 deletions lib/puma/runner.rb
Expand Up @@ -161,6 +161,10 @@ def start_server
server.tcp_mode!
end

if @options[:early_hints]
server.early_hints!
end

unless development?
server.leak_stack_on_error = false
end
Expand Down
20 changes: 19 additions & 1 deletion lib/puma/server.rb
Expand Up @@ -81,7 +81,7 @@ def initialize(app, events=Events.stdio, options={})
@precheck_closing = true
end

attr_accessor :binder, :leak_stack_on_error
attr_accessor :binder, :leak_stack_on_error, :early_hints

forward :add_tcp_listener, :@binder
forward :add_ssl_listener, :@binder
Expand Down Expand Up @@ -595,6 +595,24 @@ def handle_request(req, lines)
env[RACK_INPUT] = body
env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP

if @early_hints
env[EARLY_HINTS] = lambda { |links|
fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze

links.each do |link|
fast_write client, "Link: <#{link[:link]}>; rel=preload"
if as = link[:as]
fast_write client, "; as=#{as}"
end
fast_write client, "\r\n".freeze
end

fast_write client, "\r\n".freeze
}
else
env[EARLY_HINTS] = lambda { |_| }
end

# A rack extension. If the app writes #call'ables to this
# array, we will invoke them when the request is done.
#
Expand Down
1 change: 1 addition & 0 deletions test/helper.rb
Expand Up @@ -21,6 +21,7 @@
Thread.abort_on_exception = true

require "puma"
require "puma/events"
require "puma/detect"

# Either takes a string to do a get request against, or a tuple of [URI, HTTP] where
Expand Down
55 changes: 55 additions & 0 deletions test/test_puma_server.rb
Expand Up @@ -159,6 +159,61 @@ def test_GET_with_empty_body_has_sane_chunking
assert_equal "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n", data
end

def test_early_hints_works
@server.app = proc { |env|
env['rack.early_hints'].call([{ link: "/style.css", as: "style" }, { link: "/script.js" }])
[200, { "X-Hello" => "World" }, ["Hello world!"]]
}

@server.add_tcp_listener @host, @port
@server.early_hints = true
@server.run

sock = TCPSocket.new @host, @server.connected_port
sock << "HEAD / HTTP/1.0\r\n\r\n"

data = sock.read

expected_data = (<<EOF
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload
HTTP/1.0 200 OK
X-Hello: World
Content-Length: 12
EOF
).split("\n").join("\r\n") + "\r\n\r\n"

assert_equal true, @server.early_hints
assert_equal expected_data, data
end

def test_early_hints_is_off_by_default
@server.app = proc { |env|
env['rack.early_hints'].call([{ link: "/style.css", as: "style" }, { link: "/script.js" }])
[200, { "X-Hello" => "World" }, ["Hello world!"]]
}

@server.add_tcp_listener @host, @port
@server.run

sock = TCPSocket.new @host, @server.connected_port
sock << "HEAD / HTTP/1.0\r\n\r\n"

data = sock.read

expected_data = (<<EOF
HTTP/1.0 200 OK
X-Hello: World
Content-Length: 12
EOF
).split("\n").join("\r\n") + "\r\n\r\n"

assert_nil @server.early_hints
assert_equal expected_data, data
end

def test_GET_with_no_body_has_sane_chunking
@server.app = proc { |env| [200, {}, []] }

Expand Down

0 comments on commit 67b2347

Please sign in to comment.