Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add early hints feature #1403

Merged
merged 1 commit into from Oct 4, 2017
Merged

Add early hints feature #1403

merged 1 commit into from Oct 4, 2017

Conversation

@eileencodes
Copy link
Contributor

@eileencodes eileencodes commented Aug 29, 2017

Worked with @tenderlove on adding early hints. This is still a work in progress as there are a few things we need to address, but this PR is to start the conversation.

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.

@eileencodes eileencodes force-pushed the eileencodes:early-hints branch from e2f4042 to 59764b6 Aug 29, 2017
@tenderlove
Copy link

@tenderlove tenderlove commented Aug 29, 2017

@eileencodes and I are working on pushing early hints support in to the Rack spec for version 2 (version 2 of the SPEC, not version 2 of the gem). I'd like to try experimentally implementing this in Puma first, then push support to Rails, then upstream to the Rack SPEC. IOW, I don't want this to be done in a vacuum.

Does this seem like an acceptable approach for the Puma team? I can understand if you don't want to be used for experimentation, but I would like to get this in to production systems sooner rather than later. 😊

Also, we've verified this to work with Puma + h2o.

@tenderlove
Copy link

@tenderlove tenderlove commented Aug 29, 2017

I think we're using Ruby 2.3+ specific features in the test in this PR. If the overall concept seems OK, then we'll fix it up.

@nateberkopec
Copy link
Member

@nateberkopec nateberkopec commented Aug 29, 2017

👏 Give me a while to look at it, headed to Japan soon so not sure how much time I'll have. Last resort we can talk about it at Kaigi?

@tenderlove
Copy link

@tenderlove tenderlove commented Aug 29, 2017

@nateberkopec sounds good. 😊

@nateberkopec
Copy link
Member

@nateberkopec nateberkopec commented Aug 29, 2017

I can understand if you don't want to be used for experimentation, but I would like to get this in to production systems sooner rather than later.

also I think I speak for the whole Puma team when I say that we have no problem with experimentation. hell Evan wanted to add native websockets and rewrite the entire reactor. so adding an optional feature...pretty tame 😆

Copy link
Member

@nateberkopec nateberkopec left a comment

Couple of things just for "Puma style" concerns.

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

This comment has been minimized.

@nateberkopec

nateberkopec Aug 29, 2017
Member

Why is this necessary? I guess I don't understand.

This comment has been minimized.

@tenderlove

tenderlove Aug 30, 2017

This is so that app code can run without changes even if the server is being run without early hints support. The example app that @eileencodes put in the PR description can be run whether early hints is enabled or not (it prevents the app code from writing a if env[EARLY_HINTS] conditional)

@@ -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|

This comment has been minimized.

@nateberkopec

nateberkopec Aug 29, 2017
Member

It would be nice to have this lambda live somewhere other than the body of this method.

This comment has been minimized.

@tenderlove

tenderlove Aug 30, 2017

Agree. @eileencodes and I were trying to move it out of here, but the lambda body needs to access the io object (client) and fast_write is a private method. I'd love to move the lambda out of here, but I am not sure how.

@@ -241,6 +241,10 @@ def tcp_mode!
@options[:mode] = :tcp
end

def early_hints!

This comment has been minimized.

@nateberkopec

nateberkopec Aug 29, 2017
Member

I know our DSL style is really inconsistent, but for futureproofing I'd prefer:

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

This comment has been minimized.

@eileencodes

eileencodes Sep 20, 2017
Author Contributor

Fixed 👍

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

This comment has been minimized.

@nateberkopec

nateberkopec Aug 29, 2017
Member

Should we auto-detect this part? I did some of this in rack-http-preload..

This comment has been minimized.

@tenderlove

tenderlove Aug 30, 2017

Unsure. h2o pushes the resource regardless. I think this is something we need to iterate on. (IOW, I don't have an answer)

This comment has been minimized.

@nateberkopec

nateberkopec Aug 30, 2017
Member

It's also possible we may not want to auto-detect as at the puma level. I just wanted to ask the question.

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

def early_hints!

This comment has been minimized.

@nateberkopec

nateberkopec Aug 29, 2017
Member

I think you were copying tcp_mode!, but that's a ! method because it doesn't make sense for a server to be in tcp_mode and then not be in tcp mode later. Although I can't think of why anyone ever would (restarting and disabling early hints as the config changes?), it's more plausible that a server could toggle its support of early_hints. So this should probably just be treated like a straight-up accessor.

This comment has been minimized.

@eileencodes

eileencodes Sep 20, 2017
Author Contributor

Moved to an attr_accessor and deleted this method.


data = sock.read

assert_equal <<~eos.split("\n").join("\r\n") + "\r\n\r\n", data

This comment has been minimized.

@nateberkopec

nateberkopec Aug 29, 2017
Member

Rubocop is barfing on your squiggly heredoc.

This comment has been minimized.

@tenderlove

tenderlove Aug 30, 2017

I saw the error, and I think RuboCop needs to be fixed. This is new syntax in Ruby 2.3. But also, since this is new syntax in Ruby 2.3, and you probably want to run the Puma tests on older versions of Ruby anyway, we'll fix it. 😉

This comment has been minimized.

@eileencodes

eileencodes Sep 20, 2017
Author Contributor

Fixed the tests here too. 👍 I also added a test to test that early hints is off by default.

@nateberkopec nateberkopec self-assigned this Aug 29, 2017
@tenderlove
Copy link

@tenderlove tenderlove commented Sep 7, 2017

@eileencodes eileencodes force-pushed the eileencodes:early-hints branch 2 times, most recently from 67b2347 to d444ed3 Sep 20, 2017
@nateberkopec
Copy link
Member

@nateberkopec nateberkopec commented Sep 22, 2017

Need to look at this one more time when I'm not jetlagged but I think this is ready.

@eileencodes eileencodes changed the title WIP: Add early hints feature Add early hints feature Sep 23, 2017
@eileencodes eileencodes force-pushed the eileencodes:early-hints branch 2 times, most recently from c55bf5c to 4b87272 Sep 26, 2017
@tenderlove
Copy link

@tenderlove tenderlove commented Sep 28, 2017

Hey folks. I've been talking to @matthewd about this feature. He convinced me that the lambda should take a header hash just like the header hash from a rack response. This would alleviate webservers from knowing how the Link header is formatted, and may allow us to send other headers (like X- headers) for debugging purposes.

IOW, we should change the API to be something like this:

env[EARLY_HINTS].call(“Link”: [“<...>; rel=preload; as=..”, “..”])

WDYT?

@nateberkopec
Copy link
Member

@nateberkopec nateberkopec commented Oct 3, 2017

@tenderlove If I'm reading the HTTP 103 spec correctly:

A server MUST NOT include Content-Length, Transfer-Encoding, or any
hop-by-hop headers ([RFC7230], section 6.1) in the informational
response using the status code.
A client MAY speculatively evaluate the headers included in the
informational response while waiting for the final response. For
example, a client may recognize the link header of type preload and
start fetching the resource. However, the evaluation MUST NOT affect
how the final response is processed; the client must behave as if it
had not seen the informational response.

So, 103 status CANNOT contain a Connection, Content-Length or Transfer-Encoding header, but MAY contain any other header?

If I'm getting that right, your change makes sense. But should anyone be responsible for omitting the prohibited headers? Puma's job or someone elses?

@tenderlove
Copy link

@tenderlove tenderlove commented Oct 3, 2017

If I'm getting that right, your change makes sense. But should anyone be responsible for omitting the prohibited headers? Puma's job or someone elses?

I think if you wanted to omit them in Puma it would be fine. However, I'd expect the proxy to complain or raise an error if it got those headers, so maybe Puma shouldn't care? It would be nice for the user to get an exception closer to where the error is occurring, but I don't think it's necessary.

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>; rel=preload; as=style\n</script.js>; rel=preload")
      [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`.

If `ENV['rack.early_hints']` is not set then early hints is not
supported by the webserver. Early hints is off by default.
@eileencodes eileencodes force-pushed the eileencodes:early-hints branch from 4b87272 to ea37ada Oct 3, 2017
@eileencodes
Copy link
Contributor Author

@eileencodes eileencodes commented Oct 3, 2017

I've updated the PR so that now the lambda now takes a header hash and tested this in the Rails implementation for our test app. Let me know if you'd like other changes but I think this is good to go now.

@nateberkopec nateberkopec merged commit 0169974 into puma:master Oct 4, 2017
2 checks passed
2 checks passed
continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
eileencodes added a commit to eileencodes/rails that referenced this pull request Oct 4, 2017
When puma/puma#1403 is merged Puma will support the Early Hints status
code for sending assets before a request has finished.

While the Early Hints spec is still in draft, this PR prepares Rails to
allowing this status code.

If the proxy server supports Early Hints, it will send H2 pushes to the
client.

This PR adds a method for setting Early Hints Link headers via Rails,
and also automatically sends Early Hints if supported from the
`stylesheet_link_tag` and the `javascript_include_tag`.

Once puma supports Early Hints the `--early-hints` argument can be
passed to the server to enable this or set in the puma config with
`early_hints(true)`. Note that for Early Hints to work
in the browser the requirements are 1) a proxy that can handle H2,
and 2) HTTPS.

To start the server with Early Hints enabled pass `--early-hints` to
`rails s`.

This has been verified to work with h2o, Puma, and Rails with Chrome.

The commit adds a new option to the rails server to enable early hints
for Puma.

Early Hints spec:
https://tools.ietf.org/html/draft-ietf-httpbis-early-hints-04

[Eileen M. Uchitelle, Aaron Patterson]
khall added a commit to khall/rails that referenced this pull request Oct 4, 2017
When puma/puma#1403 is merged Puma will support the Early Hints status
code for sending assets before a request has finished.

While the Early Hints spec is still in draft, this PR prepares Rails to
allowing this status code.

If the proxy server supports Early Hints, it will send H2 pushes to the
client.

This PR adds a method for setting Early Hints Link headers via Rails,
and also automatically sends Early Hints if supported from the
`stylesheet_link_tag` and the `javascript_include_tag`.

Once puma supports Early Hints the `--early-hints` argument can be
passed to the server to enable this or set in the puma config with
`early_hints(true)`. Note that for Early Hints to work
in the browser the requirements are 1) a proxy that can handle H2,
and 2) HTTPS.

To start the server with Early Hints enabled pass `--early-hints` to
`rails s`.

This has been verified to work with h2o, Puma, and Rails with Chrome.

The commit adds a new option to the rails server to enable early hints
for Puma.

Early Hints spec:
https://tools.ietf.org/html/draft-ietf-httpbis-early-hints-04

[Eileen M. Uchitelle, Aaron Patterson]
@jumph4x
Copy link

@jumph4x jumph4x commented Oct 5, 2017

ilu guys

fast_write client, "#{k}: #{v}\r\n"
end
else
fast_write client, "#{k}: #{v}\r\n"

This comment has been minimized.

@PikachuEXE

PikachuEXE Mar 23, 2018

Should this be fast_write client, "#{k}: #{vs}\r\n" instead?
I don't see v defined in this level

This comment has been minimized.

@eileencodes

eileencodes Mar 28, 2018
Author Contributor

Oh i see what you're saying - I will fix it

PikachuEXE added a commit to PikachuEXE/passenger that referenced this pull request Apr 4, 2018
This is similar to puma/puma#1403
PikachuEXE added a commit to PikachuEXE/passenger that referenced this pull request Apr 4, 2018
This is similar to puma/puma#1403
CamJN added a commit to phusion/passenger that referenced this pull request May 7, 2018
This is similar to puma/puma#1403
@ioquatix
Copy link

@ioquatix ioquatix commented Feb 9, 2019

Was this ever integrated into the Rack SPEC? I had a look but couldn't see anything on the latest master branch.

What was the final design of the API?

I'm implementing this in falcon and support H2 server push directly so it would be fun to compare.

@ioquatix
Copy link

@ioquatix ioquatix commented Feb 9, 2019

I implemented the design that was merged into puma.

socketry/falcon@248d0a4

Here is my brief feedback:

  • It's very HTTP/1 centric (i.e. using headers). For HTTP/2, I need to parse the headers and turn that into push requests. I guess it's okay. I don't know how I'd handle other headers correctly.
  • I only add the lambda to the rack env when it's possible to send early hints. So, it's conditionally enabled. Is that okay? Because I don't bother to support 103 Early Hints yet, but only support H2 push promises.
  • I wonder if exposing this in the application layer is really a good idea. I feel like a sufficiently intelligent server could figure out what things to send and when, and additionally cache what the client has/has not received.

In summary: is this interface too generic? Would it be better to limit it to push-promise functionality, or is it good to have it allow any header?

@ioquatix
Copy link

@ioquatix ioquatix commented Feb 10, 2019

I wrote up a brief summary regarding implementing this feature.

https://www.codeotaku.com/journal/2019-02/falcon-early-hints/index

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

6 participants
You can’t perform that action at this time.