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['EARLY_HINTS'].call([{ link: "/style.css", as: "style" }, { link: "/script.js" }])
      [200, { "X-Hello" => "World" }, ["Hello world!"]]
    else
      [200, { "X-Hello" => "World" }, ["NEAT! #{ENV['REQUEST_PATH']}"]]
    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 Aug 29, 2017
1 parent 5183923 commit e2f4042
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 0 deletions.
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 @@ -220,5 +220,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!
@options[:early_hints] = true
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
23 changes: 23 additions & 0 deletions lib/puma/server.rb
Expand Up @@ -77,6 +77,7 @@ def initialize(app, events=Events.stdio, options={})
ENV['RACK_ENV'] ||= "development"

@mode = :http
@early_hints = false

@precheck_closing = true
end
Expand All @@ -97,6 +98,10 @@ def tcp_mode!
@mode = :tcp
end

def early_hints!
@early_hints = true
end

# On Linux, use TCP_CORK to better control how the TCP stack
# packetizes our stream. This improves both latency and throughput.
#
Expand Down Expand Up @@ -595,6 +600,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
26 changes: 26 additions & 0 deletions test/test_puma_server.rb
Expand Up @@ -159,6 +159,32 @@ 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!
@server.run

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

data = sock.read

assert_equal <<~eos.split("\n").join("\r\n") + "\r\n\r\n", data
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
eos
end

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

Expand Down

0 comments on commit e2f4042

Please sign in to comment.