Subject: Re: [#153] PUSH_PROMISE headers

From: Martin Thomson <[email-masked]>
Date: Mon, 1 Jul 2013 10:44:43 -0700

On 29 June 2013 07:27, James M Snell <[email-masked]> wrote:
> On Jun 29, 2013 1:03 AM, "Amos Jeffries" <[email-masked]> wrote:
>> The cache will store the pushed response under:
>>   hash(URL)+hash(Vary:"image/jpeg")
>>
>> When looking up future HIT the cache for this client on explicit-GET it
>> will look up
>>   hash(URL)+hash(Vary:"text/html, image/jpeg, image/gif")
>>
>> I assume you can spot the difference.
>
> I didn't miss that. Pushed resources and content negotiation used together
> have rather, um, "interesting" complications. However, those don't change
> the request headers in the push promise and response headers in the HEADERS
> question which is what I was addressing.

I think that we've reached a conclusion.  One very different to what
is described in SPDY/3 (the version we inherited).

A client without a cache entry will not care about what the cache key
was.  A client that has already received the document is likely to get
a cache miss due to the above effect, but at least the behavior is
"correct".  Subsequent pushes, assuming that the server is consistent
with itself, should produce a cache hit.

I can see a future for UA-sniffing here.  that makes me a little sad,
but optimizations tend to have that effect.

BTW, the idea of RST_STREAM+If-None-Match is appealing.  I've seen
lots of approaches to this problem and that is somewhat novel.  I
haven't thought it through fully, but it could add something,
depending in a number of conditions.  Like any optimization, it does
need to be tested out, and it's probably early days for that sort of
tweak.

---

From: Mike Bishop <[email-masked]>
Date: Mon, 1 Jul 2013 18:44:30 +0000

I had previously read the spec as sending a preview of the response headers, and this still feels more reasonable to me than the server sending request headers to the client, particularly a request the client wouldn't have actually made (If-Match for your GET below).  I'll concede it may be equally unnatural for the server to be sending back the resource URI in response headers, and a double header-block is also unappealing.

There are some wasted bytes if the server sends back the entire request headers, since the headers are supposed to be copied from the initial request and the client already knows what it sent.  Are we presuming that the server is sending back only the headers whose values it wants to override?  (If so, how does the server express that it wants to drop a header, override it to empty?)

Sending request headers does offer a solution to the case where the server includes a Set-Cookie in the response for the initial request and would expect that cookie to be in any subsequent requests as well - it could just inform the client that it used the new cookie in the pushed requests, potentially before the client has even seen that Set-Cookie.

From: Roberto Peon [mailto:[email-masked]]
Sent: Friday, June 28, 2013 10:54 PM
To: James M Snell
Cc: Martin Thomson; HTTP Working Group
Subject: Re: [#153] PUSH_PROMISE headers

Actually, I think you're right, since the resource is identified by the URL name (unfortunate in some ways). I was thinking that you'd want to indicate both the canonical name for the served resource and the name used to identify the request, but that'd be a bigger change.
-=R

On Fri, Jun 28, 2013 at 10:44 PM, James M Snell <[email-masked]<mailto:[email-masked]>> wrote:
I just gave an example that uses content-negotiation so that argument
doesn't really work. The server is crafting the request headers for an
implied GET, so it gets to construct those request headers to be as
specific as it wants them to be, without much need for actual
content-negotiation.

For example, suppose the server receives a request for an HTML page like this...

GET /index.html HTTP/1.1
Accept: text/html, image/jpeg, image/gif

The server decides that it wants to push jpeg files to the client... it sends

PUSH_PROMISE
  :path = /images/f.jpg
  :method = GET
  :host: example.org<http://example.org>
  :if-match: "my-etag1"
  accept: image/jpeg

That's pretty darn unambiguous. I can easily check to see if I have a
representation of "/images/f.jpg" with etag "my-etag1" and
content-type "image/jpeg" in my local cache, without the need to have
*any* response headers in the PUSH_PROMISE.

Perhaps you have another, more specific example in mind?

On Fri, Jun 28, 2013 at 10:36 PM, Roberto Peon <[email-masked]<mailto:[email-masked]>> wrote:
> ... because by then you've opened up a stream., and you're back into
> problematic territorry.
> PUSH_PROMISE exists because we need to indicate to the browser all of the
> information it needs to make a determination about whether or not it wants
> the stream (and to short circuit the inlining/push mechanism when it already
> has what it needs!)
> -=R
>
>
> On Fri, Jun 28, 2013 at 10:34 PM, Roberto Peon <[email-masked]<mailto:[email-masked]>> wrote:
>>
>> Any content negotiation would be an appropriate example. :)
>>
>> You don't want to have to wait for the HEADERS frame to indicate to the
>> client which resource it might already have (it should have the opportunity
>> to RST_STREAM if it has it in cache, for instance).
>> -=R
>>
>>
>> On Fri, Jun 28, 2013 at 10:25 PM, James M Snell <[email-masked]<mailto:[email-masked]>> wrote:
>>>
>>> Have an example handy?
>>>
>>> Here's an example that shows that response headers in the PUSH_PROMISE
>>> would not be necessary... Let's say I send a PUSH_PROMISE with the
>>> following bits of info...
>>>
>>> PUSH_PROMISE
>>>   :path = /images/f.jpg
>>>   :method = GET
>>>   :host = example.org<http://example.org>
>>>   :scheme = http
>>>   accept = image/jpeg
>>>   if-match: "my-etag1"
>>>   cache-control: max-age=1000
>>>
>>> These headers are giving me everything I would need to determine if
>>> there is a matching resource in my local cache. I have the method, I
>>> have the etag, I have the cache-control parameters, accept... There's
>>> no need for response headers at this point.
>>>
>>> Later, once I start accepting the frames for the pushed content, I
>>> would get something like...
>>>
>>> HEADERS
>>>   :status = 200
>>>   content-type: image/jpeg
>>>   content-length: 123
>>>   etag: "my-etag1"
>>>   vary: accept
>>>   cache-control: public
>>>
>>> On the off chance that the PUSH_PROMISE doesn't give me what I need,
>>> the follow on HEADERS frame will give me the rest.
>>>
>>>
>>>
>>> On Fri, Jun 28, 2013 at 9:55 PM, Roberto Peon <[email-masked]<mailto:[email-masked]>> wrote:
>>> > Depending on how the request might have been been constructed, response
>>> > headers may be necessary to identify the resource in the cache, as
>>> > compared
>>> > to the resource specified in the HTML (I'm thinking about vary: stuff).
>>> >
>>> > -=R
>>> >
>>> >
>>> > On Fri, Jun 28, 2013 at 9:44 PM, James M Snell <[email-masked]<mailto:[email-masked]>>
>>> > wrote:
>>> >>
>>> >> Let's take a step back and consider what a pushed stream is...
>>> >>
>>> >> A pushed stream is essentially an "Implied GET". This means that a
>>> >> server is going to assume that the client was going to send a GET for
>>> >> the pushed resource. This also means that the server has to make some
>>> >> assumptions about the make up of that implied GET.
>>> >>
>>> >> Now, consider how HTTP caching works. When a cache receives a request
>>> >> for a resource, how does it determine whether or not it has a
>>> >> representation of the resource already available? Does it look at the
>>> >> request headers or the response headers? Obviously, it looks at the
>>> >> request headers. It uses the response headers when populating the
>>> >> cache.
>>> >>
>>> >> So, if we look at the pushed resource sent by the server, what we need
>>> >> is for A) the server to first let us know about the implied GET
>>> >> request.. which means pushing down a set of request headers then B)
>>> >> the server to send the actual resource, which means pushing down the
>>> >> response headers.
>>> >>
>>> >> Already in our design for pushed resources, we have the server sending
>>> >> a PUSH_PROMISE frame that contains a header block, followed by a
>>> >> HEADERS frame that also contains a headers block. It stands to reason
>>> >> that the PUSH_PROMISE frame would contain the set of request headers
>>> >> that the server is assuming for the implied GET. These are delivered
>>> >> to the client, which uses those to determine whether or not a cached
>>> >> representation of the resource is already available (just as any cache
>>> >> would do using the request headers). The server would then send it's
>>> >> response headers in a HEADERS frame, just as it would any response to
>>> >> any other kind of GET.
>>> >>
>>> >> Two examples to show how this naturally fits... First, let's look at a
>>> >> normal GET request sent by the client to the server...
>>> >>
>>> >> Client                 Server
>>> >> ------                 ------
>>> >>   |                        |
>>> >>   | ---------------------> |
>>> >>   |   HEADERS              |
>>> >>   |     GET                |
>>> >>   |     /images/f.jpg      |
>>> >>   |     If-Match: etag1    |
>>> >>   |     Accept: image/jpeg |
>>> >>   |                        |
>>> >>   | <--------------------- |
>>> >>   |   HEADERS              |
>>> >>   |     200                |
>>> >>   |     Content-Type:      |
>>> >>   |       image/jpeg       |
>>> >>   |     Content-Length:    |
>>> >>   |       123              |
>>> >>   |                        |
>>> >>   | <--------------------- |
>>> >>   |   DATA....DATA....     |
>>> >>   |                        |
>>> >>
>>> >> Now consider the same resource being pushed by the server using
>>> >> PUSH_PROMISE...
>>> >>
>>> >> Client                 Server
>>> >> ------                 ------
>>> >>   |                        |
>>> >>   | <--------------------- |
>>> >>   |   PUSH_PROMISE         |
>>> >>   |     GET                |
>>> >>   |     /images/f.jpg      |
>>> >>   |     If-Match: etag1    |
>>> >>   |     Accept: image/jpeg |
>>> >>   |                        |
>>> >>   | <--------------------- |
>>> >>   |   HEADERS              |
>>> >>   |     200                |
>>> >>   |     Content-Type:      |
>>> >>   |       image/jpeg       |
>>> >>   |     Content-Length:    |
>>> >>   |       123              |
>>> >>   |                        |
>>> >>   | <--------------------- |
>>> >>   |   DATA....DATA....     |
>>> >>   |                        |
>>> >>
>>> >>
>>> >> Note that the only difference here is the direction and type of the
>>> >> first frame. Everything else is identical. The PUSH_PROMISE contains
>>> >> everything the client needs to determine whether or not it already has
>>> >> the resource in it's local cache (request URI, etag, content-type...).
>>> >>
>>> >> There's no need to get any more complicated than this. We already
>>> >> require two distinct header blocks for every request. We already send
>>> >> two distinct header blocks for each pushed stream. We already indicate
>>> >> that a pushed stream is an implied GET. To make it work, we simply
>>> >> state that the PUSH_PROMISE contains the Request headers that the
>>> >> server has assumed for the implied GET request, while the HEADERS
>>> >> frame sent later contains the Response headers. If the request headers
>>> >> in the PUSH_PROMISE end up not being adequate enough to properly
>>> >> determine if the resource is already cached, then we treat it as just
>>> >> another cache miss.
>>> >>
>>> >> On Fri, Jun 28, 2013 at 5:21 PM, Martin Thomson
>>> >> <[email-masked]<mailto:[email-masked]>> wrote:
>>> >> > https://github.com/http2/http2-spec/issues/153
>>> >> >
>>> >> > The current text describes PUSH_PROMISE as having a few request
>>> >> > headers, plus some response headers, but it's quite vague.
>>> >> >
>>> >> > I think that if this is going to be properly workable across a wide
>>> >> > range of uses with lots of different headers, PUSH_PROMISE needs to
>>> >> > include two sets of headers: the ones that it overrides from the
>>> >> > associated request (:path being foremost of those) and the ones that
>>> >> > it provides as a "preview" of the response (e.g., ETag might allow
>>> >> > caches to determine if they were interested in the rest of the
>>> >> > response).
>>> >> >
>>> >>
>>> >
>>
>>
>

---

From: Martin Thomson <[email-masked]>
Date: Mon, 1 Jul 2013 13:01:48 -0700

On 1 July 2013 11:44, Mike Bishop <[email-masked]> wrote:
> There are some wasted bytes if the server sends back the entire request
> headers, since the headers are supposed to be copied from the initial
> request and the client already knows what it sent.  Are we presuming that
> the server is sending back only the headers whose values it wants to
> override?  (If so, how does the server express that it wants to drop a
> header, override it to empty?)

I think that this is the bit that will need additional refinement.  As
this describes, the headers that are included in PUSH_PROMISE replace
headers (or add new ones) that are in the associated request.

That leads to some interesting questions with respect to compression.
Maybe the right way to do this is to have PUSH_PROMISE include ALL
headers from the associated request, and then use that associated
request as the reference from which compression builds.

Of course, that would be grossly premature, given how little we know
of usage patterns for push.

---

From: James M Snell <[email-masked]>
Date: Mon, 1 Jul 2013 13:20:36 -0700

Let's please not conflate compression and push.. they really are
unrelated to one another.

There is really only two reasons for including header fields in a push promise:

1. Identifying the resource being pushed
2. Allowing the recipient to determine if it needs the pushed resource

In HTTP, we already have a client-side caching mechanism that is based
entirely on comparison of a relevant set of request headers against
what is currently stored in the cache. If a new items it added to the
cache, the response headers are used to populate, but it's the set of
request headers that are used to determine cache hits or misses. This
is a pre-existing mechanism that is already very well established.

For the push promise, the request headers give us everything we need
to address #1 and #2 above. Sending the request headers in the
push_promise gives the client side the context it needs to understand
the pushed set of response frames.

Yes, it's understood that the request headers generated by the server
might not match exactly what the client may have sent for the same
resource but that's perfectly ok... This *is* server push after all...
the request headers sent in the push promise would be the request
headers the server has implied for the pushed streams *implied* GET.

The idea of the push promise sending a "preview" of the response
headers is rather nonsensical because (a) the response headers do not
help me address the two goals above (unless they are required to
contain a content-location header) and (b) why send a preview of the
response headers moments before sending the actual response headers?

The idea then, is this: Sending a push promise is the servers way of
saying, "Based on the initial request, I expect that you're going to
send another request that looks like this..." [where "this" is the
assumed set of request headers ] "... therefore I'm going to go ahead
and send the appropriate response without waiting for you to send it."

Compression of the headers does not enter into the discussion in any
way. Compression only deals with how the bits are serialized on the
wire in each direction, not the actual content or use of the headers.

As a future potential optimization, as part of the originating request
sent by the client, I can imagine someone eventually defining a set of
"push negotiation" request headers... something like...

  Push-Accept: image/jpeg, image/png
  Push-If-Match: "resource-1-etag", "resource-2-etag"
  Push-If-Modified-Since: 2013-12-12T12:12:12Z

Which would be interpreted as:

  1. I'll except pushes for JPEG and PNG resources
  2. So long as their entity tags match "resource-1-etag" or "resource-2-etag"
  3. And have been modified after 2013-12-12T12:12:12Z

These kinds of optimizations would make sense *after* we get a lot
more experience with push. Until then, however, having the simple rule
that for pushed streams "PUSH_PROMISE contains Request Headers and
HEADERS contains Response Headers" is simple enough for us to get
started. We can tweak it further later once we get more experience.

On Mon, Jul 1, 2013 at 1:01 PM, Martin Thomson <[email-masked]> wrote:
[snip]

---

From: Martin Thomson <[email-masked]>
Date: Mon, 1 Jul 2013 13:31:05 -0700

> unrelated to one another.

Ahh, but all optimizations ultimately do that sort of conflation.
You'll note that it was pure speculation, with a "not-now-please"
qualifier.

I think that we have an answer to this question already.  Request
headers have to be sufficient to determine if a cache has the answer,
otherwise you would always require validation.  (And push sort-of
end-runs the validation thing too.)

The only model we need is: PUSH_PROMISE == "this is the request I
synthesized for you".  Then, we can do all sorts of things to control
how that synthesis is controlled.  Though "not-now-please" still seems
appropriate.

---

From: Sam Pullara <[email-masked]>
Date: Mon, 1 Jul 2013 13:59:16 -0700

I would avoid conflating the client caching semantics with the request identifying push headers. In fact, I would err on the side simplicity for this mechanism. For example, since these subsequent requests are based on the initial page you can likely use only the URL to identify the pushed resource. Both a browser and an API client should have a well defined way of tracking subsequent requests back to this parent request. Varying the pushed resources based on different request headers rather than just the URL and parent request seems fraught with edge cases.

A real question I have is whether it even makes sense to push the content right away when you could just send down the list of resources? You'd only lose one RTT and depending on network speed and size of the initial request that could end up being net zero additional latency.

Sam

---

From: Martin Thomson <[email-masked]>
Date: Mon, 1 Jul 2013 16:37:47 -0700

On 30 June 2013 20:51, Amos Jeffries <[email-masked]> wrote:
> There does seem to be an interesting case for RST_STREAM+If-Not-Modified
> here to optimize bandwidth usage.

I don't think that we need to standardize this, but servers could just
include If-Match in their PUSH_PROMISES.  Then clients have an etag
upon which to base their RST_STREAM decisions.

Of course, that would depend on the server wanted to encourage clients
to send RST_STREAM.  No point encouraging RST_STREAM if it's only ever
going to arrive late.

---

From: Mike Bishop <[email-masked]>
Date: Tue, 2 Jul 2013 20:24:20 +0000

Did we have consensus here on whether PUSH_PROMISE in the Implementation Draft contains *all* request headers or simply the ones that have changed from the original request?

The more I think about it, I'm inclined to say "all" since the server can drop any headers it knows it won't be looking at if it wants, and headers that are the same will just get compressed out after they've been pushed back once.

That seems simpler than trying to contort header compression to change what the initial context is.

-----Original Message-----
From: Martin Thomson [mailto:[email-masked]] 
Sent: Monday, July 1, 2013 1:02 PM
To: Mike Bishop
Cc: Roberto Peon; James M Snell; HTTP Working Group
Subject: Re: [#153] PUSH_PROMISE headers

On 1 July 2013 11:44, Mike Bishop <[email-masked]> wrote:
> There are some wasted bytes if the server sends back the entire 
> request headers, since the headers are supposed to be copied from the 
> initial request and the client already knows what it sent.  Are we 
> presuming that the server is sending back only the headers whose 
> values it wants to override?  (If so, how does the server express that 
> it wants to drop a header, override it to empty?)

I think that this is the bit that will need additional refinement.  As this describes, the headers that are included in PUSH_PROMISE replace headers (or add new ones) that are in the associated request.

That leads to some interesting questions with respect to compression.
Maybe the right way to do this is to have PUSH_PROMISE include ALL headers from the associated request, and then use that associated request as the reference from which compression builds.

Of course, that would be grossly premature, given how little we know of usage patterns for push.

---

From: James M Snell <[email-masked]>
Date: Tue, 2 Jul 2013 13:29:55 -0700

I would say, "All request headers the server has assumed for the
implicit request". The difference is subtle but worth it.

---

From: Mike Bishop <[email-masked]>
Date: Tue, 2 Jul 2013 20:44:20 +0000

Perhaps too subtle for me.  :-)  Sorry to be pedantic, but that means that the only headers on the synthetic request are those which are contained in the PUSH_PROMISE?

If so, doesn't that simplify away the text about "required" headers in PUSH_PROMISE, since they're the same required headers in any HTTP Request?

-----Original Message-----
From: James M Snell [mailto:[email-masked]] 
Sent: Tuesday, July 2, 2013 1:30 PM
To: Mike Bishop
Cc: Martin Thomson; Roberto Peon; HTTP Working Group
Subject: Re: [#153] PUSH_PROMISE headers

I would say, "All request headers the server has assumed for the implicit request". The difference is subtle but worth it.

On Tue, Jul 2, 2013 at 1:24 PM, Mike Bishop <[email-masked]> wrote:
>> values it wants to override?  (If so, how does the server express 
>> that it wants to drop a header, override it to empty?)
>

---

From: Roberto Peon <[email-masked]>
Date: Tue, 2 Jul 2013 13:50:02 -0700

We haven't even had substantive discussions about it yet.

My preference is that we stick everything in there, since otherwise you are
forced to keep the state from the original request around indefinitely, and
that contradicts the intent of PUSH_PROMISE.
The intent of PUSH_PROMISE is that any state allocation done because of
PUSH_PROMISE is minimal and can be dropped safely without causing
correctness issues (you may be getting the resource twice at worst).

-=R

On Tue, Jul 2, 2013 at 1:44 PM, Mike Bishop <[email-masked]>wrote:

> Perhaps too subtle for me.  :-)  Sorry to be pedantic, but that means that
> the only headers on the synthetic request are those which are contained in
> the PUSH_PROMISE?
>
> If so, doesn't that simplify away the text about "required" headers in
> PUSH_PROMISE, since they're the same required headers in any HTTP Request?
>
> I would say, "All request headers the server has assumed for the implicit
> request". The difference is subtle but worth it.
>
>
> > Did we have consensus here on whether PUSH_PROMISE in the Implementation
> Draft contains *all* request headers or simply the ones that have changed
> from the original request?
> >
> > The more I think about it, I'm inclined to say "all" since the server
> can drop any headers it knows it won't be looking at if it wants, and
> headers that are the same will just get compressed out after they've been
> pushed back once.
> >
> headers from the associated request, and then use that associated request
> as the reference from which compression builds.
> >

---

From: Amos Jeffries <[email-masked]>
Date: Wed, 03 Jul 2013 09:42:46 +1200

> The more I think about it, I'm inclined to say "all" since the server can drop any headers it knows it won't be looking at if it wants, and headers that are the same will just get compressed out af…

Except that they are being sent over the response side of the connection 
so they are affecting the server send context and client receive 
context. They are also likely to die again quickly under the relatively 
large bunch of response HEADERS blocks which are following - both the 
main response and the pushed ones.

I'm sightly in favour of "all" as well despite that problem because when 
sending over multiple hops the request headers may be altered. The 
server omiting header Foo because a proxy delivered Foo:hello when the 
client sent Foo:bar will cause big potential issues with both security 
validation and caching. Ensuring those functionalities work reliably 
outweighs some sub-optimal performance IMO.

Amos

---

From: Roberto Peon <[email-masked]>
Date: Tue, 2 Jul 2013 16:13:20 -0700

That problem would be assuming that the proxy doesn't correctly translate
when generating its push, should it do so, but yes.
Being explicit for request headers for push has (or should have) a low cost
given the compression stuff, and makes life much more simple in general
otherwise.
-=R

On Tue, Jul 2, 2013 at 2:42 PM, Amos Jeffries <[email-masked]> wrote:

>> The more I think about it, I'm inclined to say "all" since the server can
>> drop any headers it knows it won't be looking at if it wants, and headers
>> that are the same will just get compressed out after they've been pushed
>> back once.
>>
>
> so they are affecting the server send context and client receive context.
> They are also likely to die again quickly under the relatively large bunch
> of response HEADERS blocks which are following - both the main response and
> the pushed ones.
>
> sending over multiple hops the request headers may be altered. The server
> omiting header Foo because a proxy delivered Foo:hello when the client sent
> Foo:bar will cause big potential issues with both security validation and
> caching. Ensuring those functionalities work reliably outweighs some
> sub-optimal performance IMO.
>
> Amos
>
>
>

---

From: William Chan (陈智昌) <[email-masked]>
Date: Tue, 2 Jul 2013 16:31:46 -0700

+1

On Tue, Jul 2, 2013 at 4:13 PM, Roberto Peon <[email-masked]> wrote:

> Being explicit for request headers for push has (or should have) a low
> cost given the compression stuff, and makes life much more simple in
> general otherwise.
> -=R
>
>
> On Tue, Jul 2, 2013 at 2:42 PM, Amos Jeffries <[email-masked]>wrote:
>