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

Define a cache for H2 server push & friends #354

Closed
annevk opened this issue Aug 4, 2016 · 36 comments
Closed

Define a cache for H2 server push & friends #354

annevk opened this issue Aug 4, 2016 · 36 comments

Comments

@annevk
Copy link
Member

annevk commented Aug 4, 2016

We have several features that interact with some cache that is not really defined:

  • rel=prefetch
  • rel=preload
  • H2 server push

I think the approximate semantics we want are HTTP cache semantics. However, resources that cannot be cached should only survive the lifetime of the fetch group and can only be fetched within the scope of that fetch group (once?).

@jakearchibald
Copy link
Collaborator

jakearchibald commented Aug 4, 2016

I agree with your thinking here.

On document-insertion <link rel="preload"> creates an entry in the fetch group's "preload cache" containing the request and a promise for the response. Should we do this for no-store responses too? Like all requests, their fetch goes through the service worker with a "default" cache mode.

Then before step 3 of https://fetch.spec.whatwg.org/#http-fetch: (with lots of hand-waving)

  1. If the request mode is not "navigation":
    1. For each entry in the client's fetch group's "preload cache":
      1. If the request's url and method matches the entry's request
        1. Let preloadResponse be the result of waiting for the entry's response promise
        2. If this entry is no longer in the fetch group's "preload cache", skip to the next iteration of the for-each (trying to catch races here)
        3. If preloadResponse is a network error (do we want to do this for 404s etc too?)
          1. Remove this entry from the "preload cache"
          2. Skip to the next iteration of the for-each
        4. For the vary header in preloadResponse, if request and the entry's request match up
          1. Remove this entry from the "preload cache"
          2. Return preloadResponse

H2 server push

Can these be easily associated with a fetch group? I thought a single H2 connection would be used for multiple pages.

@jakearchibald
Copy link
Collaborator

In the above I'm trying to ensure that the preload cache is used even if the request is still in-progress.

@annevk
Copy link
Member Author

annevk commented Aug 4, 2016

@jakearchibald a H2 server push is part of an HTTP response to an HTTP request. So it can only exist as part of a single request-response stream and not on its own. So therefore you should always be able to associate the push promises with a fetch group.

@jakearchibald
Copy link
Collaborator

jakearchibald commented Aug 4, 2016

You're right, the PUSH_PROMISE has a stream ID for itself along with the stream ID for its data. In that case I agree that it should match <link rel="preload">.

I believe we don't allow pushed items into the HTTP cache until they're "used". This doesn't make a whole lot of sense to me, but maybe @igrigorik can explain why.

@bzbarsky
Copy link

bzbarsky commented Aug 4, 2016

However, resources that cannot be cached should only survive the lifetime of the fetch group

That's not correct in the case of <link rel=prefetch>.

@annevk, please make sure that you talk to some of the network team folks at Mozilla about this; I'm not going to have the bandwidth to be involved in this conversation...

@jakearchibald
Copy link
Collaborator

It'd be a lot simpler if prefetch only populated the HTTP cache (unless no-store etc). prerender is already confusing in terms of security w3c/resource-hints#63.

@jakearchibald
Copy link
Collaborator

https://groups.google.com/a/chromium.org/forum/#!topic/loading-dev/GoONR_xSGJ4

In Chrome, pushed resources don't enter the http cache unless they're matched up to a future request.

As a developer, this is pretty unexpected, but I'm keen to hear from other vendors.

@annevk
Copy link
Member Author

annevk commented Aug 18, 2016

When this was discussed during the HTTP workshop vendors seemed to be open to changing this. After all, if you want to fill up the HTTP cache there's plenty of ways to do so already. So not letting H2 server push near it is weird. The uncacheables would get the lifetime of the fetch group, per OP.

@sleevi
Copy link

sleevi commented Aug 18, 2016

@annevk I'm curious what's intended by the fetch group semantics, as I don't believe that's something implemented in Chrome (nor easily implemented). The closest parallel I can think is if it's meant to be the abstraction layer detailing how our renderer process works (and the relationship to the renderer-side memory cache), but if that's the case, then it certainly is not part of our implementation or notion of Fetch, nor would I be keen (nor would it be possible) to hang more stuff off it.

I think I'd disagree with @jakearchibald on the relationship between PUSH and the cache being weird, and it may simply have to do with the choice of verb attached to it implying a semantic meaning unmet by implementations; that is, had it been called, say, OPPORTUNISTIC_RESOURCE, would that have made it clearer or not? I'm not aware of any implementation letting H/2 into the cache, and while that may be something to discuss, I absolutely want to make sure we don't entangle it with the notion of fetch groups unless/until we have a solid understanding of what they're trying to describe or prescribe. I'm not sure if this is exactly the right bug to hash it out, and I suspect I'd need to rope in a few colleagues to make sure we're accurately describing it, but for now, I'd like to separate out protocol-handling discussions (e.g. H/2) from "Fetch/Web Platform" level discussions (e.g. prefetch, preload; whether or not that's a fair characterization I'm not sure, but that's certainly reflected in how Chrome has implemented them and architected around)

@annevk
Copy link
Member Author

annevk commented Aug 19, 2016

A fetch group is something that manages all the fetches for a document or worker and has a similar lifecycle. The precise details are still a little unclear, but we need something like that to define what happens when navigating and such.

I think it would be fine to discuss H2 server push separately from prefetch and friends. We could first try to sort out the latter and then see how to improve the former. There's also an open JavaScript API question around H2 server push that at some point will come to a head and we have better figured out the layering by then.

@wanderview
Copy link
Member

Comment on "fetch group", sorry if its off topic:

One thing we have not had to deal with in FF so far are fetches in the same fetch group (we call it a LoadGroup) that are triggered from different processes. We will have to figure this out as we move to a more multi-process architecture. But this might explain some differences between FF and chrome today. Perhaps we will end up looking more like chrome here for pragmatic reasons.

If we want to add functionality to fetch group we need to think about how they work across process boundaries. I guess its more of an implementation concern, but it will restrict what can be realized from the spec.

@jakearchibald
Copy link
Collaborator

I've typed this a few times over the past weeks, and deleted it, so it's probably still a bad idea.

The "list of available images" has a lot of similarities to the preload cache. The differences are:

  • Items aren't discarded after use, they keep matching
  • The image cache can transfer between navigations

We could try and merge these. link[rel=preload] could add options to persist the item in the cache after usage, or allow the item to persist across navigations.

@wanderview
Copy link
Member

I think a difference between something like image cache and preload cache is where they are tied into the stack. In my fuzzy view of the universe:

  • Preload cache is consulted at the network layer, similar to http cache. This would be below the service worker.
  • Image cache is consulted at the DOM layer by element loading, etc. This would be above the service worker.

Maybe that is an incorrect view of the world. I think if we want these caches to be spec'd similarly, though, we may want to align where they are accessed in the stack.

@jakearchibald
Copy link
Collaborator

I think the intent is for the preload cache to sit with the fetch group, so preload requests go through the SW.

H2 cache is currently at the network level in Chrome though.

@rektide
Copy link

rektide commented Oct 24, 2016

There's also an open JavaScript API question around H2 server push that at some point will come to a head and we have better figured out the layering by then.

For reference, #51 seems to be the general H/2 push topic and #65 has one way of fulfilling that, an observer api.

igrigorik added a commit to w3c/preload that referenced this issue Nov 23, 2016
Preload response's are one of several responses that interact with the
(yet to be formally defined) response cache. As such, the logic for both
populating and querying said cache should live in Fetch.

- Updated issue text as a warning, with some context and pointer to the
  discussion in Fetch.
- Removed "match a preloaded response" definition: this should be
  handled transparently by Fetch, as one of the first steps in its main
  fetch algorithm.

Fetch issue where we should continue this discussion:
whatwg/fetch#354

Closes #30.
igrigorik added a commit to w3c/preload that referenced this issue Nov 30, 2016
* response cache and matching lives in Fetch

Preload response's are one of several responses that interact with the
(yet to be formally defined) response cache. As such, the logic for both
populating and querying said cache should live in Fetch.

- Updated issue text as a warning, with some context and pointer to the
  discussion in Fetch.
- Removed "match a preloaded response" definition: this should be
  handled transparently by Fetch, as one of the first steps in its main
  fetch algorithm.

Fetch issue where we should continue this discussion:
whatwg/fetch#354

Closes #30.
@jakearchibald
Copy link
Collaborator

jakearchibald commented Feb 21, 2017

An interesting side-effect of Chrome's H2 push implementation:

The H2 cache is stored at the connection level, so if the connection terminates, the cache is lost. Because separate connections are used for credentialed vs non-credentialed requests, you can end up with unexpected H2 cache misses. Eg:

// This resource is /sw.js
addEventListener('install', event => {
  event.waitUntil(async function() {
    const cache = await caches.open('static-v1');
    await cache.add('/script.js');
  }());
});

If you push /script.js along with /sw.js, you get a double-download of /script.js. /sw.js is fetched with credentials and /script.js isn't, so the non-credentialed request for /script.js is a cache-miss.

The workaround is to fetch /script.js with credentials:

cache.add(new Request('/script.js', {credentials: 'include'}));

@annevk
Copy link
Member Author

annevk commented Feb 21, 2017

It has to work that way, just like the HTTP cache (see #307) otherwise you have leaks.

@yoavweiss
Copy link
Collaborator

@jakearchibald - Yup, see https://bugs.chromium.org/p/chromium/issues/detail?id=669515

@annevk - If it has to work this way, it means that no-credential fetches (fonts, ES6 modules, and SW fetched resources in Jake's example) can never be reclaimed from push, and therefore are inherently slower than their credentialed counter-parts :/

@annevk
Copy link
Member Author

annevk commented Feb 21, 2017

Right, that's a known problem without a solution (see #341).

@sleevi
Copy link

sleevi commented Feb 21, 2017 via email

@yoavweiss
Copy link
Collaborator

@sleevi - The non-credentialed connection often doesn't get created until later on, while H2 push is most effective before the HTML ever reaches the browser, and shortly after the HTML finished sending.

@jakearchibald
Copy link
Collaborator

For instance, a document's rendering may require some CSS and a font. However, you can't push the font down with the page request, because fonts are requested without credentials and pages are.

But the lack of fetch control in CSS is probably the issue here.

@jakearchibald
Copy link
Collaborator

The above is only an issue if the font is on another origin under the same certificate authority, as same-origin font requests get credentials, so it's not as common as I thought.

@mnot
Copy link
Member

mnot commented Feb 24, 2017

That's true for all same-origin requests, if I read step 4 of HTTP-network-or-cache fetch and friends correctly?

@annevk
Copy link
Member Author

annevk commented Feb 24, 2017

@mnot fetch() and <script type=module> set credentials mode to omit by default, so those would affect same-origin.

@mnot
Copy link
Member

mnot commented Feb 24, 2017

Huh. That's weird, but ok.

@mnot
Copy link
Member

mnot commented Feb 24, 2017

Back to the issue of what the purpose behind NOT using the HTTP cache for H2 PUSHed responses -- IIRC (and this is hazy), one concern was that on sites that represent more than one party (e.g., a shared web host), a hostile user could push content into the cache under another user's URLs.

However, as was pointed out at the workshop, our malicious user could easily circumvent that by just referencing them from the page that's loaded from the attacking URL; they'll be "used" by that page, and then promoted into the HTTP cache.

See also: http://httpwg.org/specs/rfc7540.html#rfc.section.10.4

I think that the browser implementers of H2 did things this way out of an abundance of caution, but the feeling at the workshop was that we could probably move past this now.

@mcmanus @martinthomson anything to add?

@sleevi
Copy link

sleevi commented Feb 24, 2017 via email

@martinthomson
Copy link
Contributor

(I'm really fuzzy on which origins we're talking about being the "same" here. There is a page origin, the request origin, and the origin of the pushed resource. All are potentially relevant in this context.)

I'm looking at fetch and it doesn't appear to be that there is any problem. I agree that there are many reasons for withholding a request, but they can all be reduced simply by observing two things:

  1. Sites can cause the browser to make a request. This is no different to generating a push promise.

  2. The existence of a response does not need to mean that the response needs to be used. Thus, all the blocking, service workers, and other conditions can easily apply before consuming the response. Worst case, the request needed a preflight; so send the preflight and hope that that was pushed as well.

I appreciate the caution, and the reasons, and the need to make some changes to accommodate the above model (if you accept that it is valid). However, I don't think that those reasons are strong enough to say that you categorically don't cache pushed resources.

@sleevi
Copy link

sleevi commented Feb 27, 2017 via email

@martinthomson
Copy link
Contributor

(Ahh, email replies in github remain a real challenge.)

I'm not sure how best to clarify the confusion, because there's only one
origin - it's a scheme/host/port tuple. It's the processing model that
varies, and that is the point.

I admit to not understanding your response at all. Let me try to explain a little more about what I was talking about.

When a site makes a request (maybe by invoking fetch), the origin of that site is relevant. Let's call that A. The target of that request (ignoring redirects) is also relevant because it determines whether that request is same origin or not. Let's call that B. But in the push case, the target of a cross-origin fetch can also push cross-origin. Let's call that C. These might all be different (a page from https://example.com makes a request to https://example.net which pushes https://api.example.net for example), but some might be the same as others. The one that bothers me most is where C==A.

If this is what you refer to when you talk about "an initiation of fetch has a context (in the page and origin) that exceeds the available information in a PUSH PROMISE", then we're probably just in agreement on the need to walk through the wrinkles.

The coalescing of HTTP/2 does not mean that
you can skip the processing model of Fetch or CORS - as to do so would
undermine the security properties and principles that the SOP is designed
to protect.

I didn't mean to imply skipping any checks. The opposite in fact. I meant to observe that you don't need to skip any checks at all and that by doing so you achieve a system with similar - though maybe not identical - properties to one where sites can request that the browser fetch things (i.e., the one we have today).

@sleevi
Copy link

sleevi commented Feb 27, 2017

If this is what you refer to when you talk about "an initiation of fetch has a context (in the page and origin) that exceeds the available information in a PUSH PROMISE", then we're probably just in agreement on the need to walk through the wrinkles.

It's not what I meant, so at least now we've found the source of confusion.

Start with the algorithm described in https://fetch.spec.whatwg.org/#http-fetch , and work through every place where "return a NetworkError" is specified. Then look at what conditions cause that. You will see there is more context than 'just' the URL.

For example, the algorithm in https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff? is dependent on https://fetch.spec.whatwg.org/#concept-request-type which is dependent on the element or context in the page that caused the fetch.

https://fetch.spec.whatwg.org/#concept-request-destination is perhaps an even clearer highlighting of the variations in the processing model that affect things during Fetch.

As it stands in the world today, where user agents do not allow direct pushes into the HTTP cache, there are a number of ways in which a request from example.com to example.net can be stopped or blocked before any information is sent or stored. A PUSH PROMISE that is allowed to enter the HTTP cache fundamentally changes that.

@mnot
Copy link
Member

mnot commented Feb 27, 2017

@sleevi - I wasn't talking about coalesced origins; for the moment, let's figure this out for same-origin (while still acknowledging that coalescing will raise its head at some point).

Thanks for giving concrete examples.

WRT should request be blocked due to nosniff, that step is run after http network or cache fetch, so it will still apply to requests if you model server push as being placed into the HTTP cache. The request type is available at request time, and will be appropriately applied to the decision. How the response actually got stored in the cache isn't really an issue AFAICT.

WRT request destination, I'm afraid it's not clear to me. HTTP caches can be and are deployed at many places in the path between the browser and the origin server, including on the same box as the browser. Server push will get into those caches (as well as other responses from a variety of request contexts), the browser will get responses from them without any notion of the internals of Fetch. How is the HTTP cache special?

I went through the rest of the Fetch spec as you asked, and didn't see any immediate problems. Can you point out any more? Happy to admit I'm missing something here, because I know you have a deeper understanding of the internals here.

I totally get that browsers may have made some optimisations in their implementations by assuming that things that successfully get into the HTTP cache have specific attributes. I can even see that from an implementation perspective, it makes sense to create a separate cache for pushes or to taint them because of historical implementation decisions. I'm just not yet seeing a reason to call it a different kind of cache in the specs yet, when the specs appear to already be written in a manner where It Just Works, more or less.

@sleevi
Copy link

sleevi commented Feb 27, 2017

@mnot I totally appreciate your perspective coming at it from the view of a server operator, because I think it's important to distinguish between what's observable to the client, what's observable to the server, what's observable to intermediates, and what's observable to the user. My premise is that any observable differences should be spec'd - whether that appears in HTTP (and related) in the side of IETF, or in Fetch (and related) from the perspective of the client here - and my focus has been on observable behaviours emitted from the client or affecting the user. This is mostly out of selfish interest - I can influence client behaviour, but I can't help server behaviours and how they cache or if they cache appropriately.

For same-origin fetches, I agree with you that it's unlikely to be problematic from either a security perspective or a network-observable perspective. This is why Chrome is exploring implementation options to better optimize the H/2 experience, and why there is still a lot of discussion around rel=preload and the H/2 cache.

In discussing the processing model, the following stand out for same-origin fetches:

  • When a Service Worker is present, it's possible that the SW will intercept all requests for /a.js and potentially rewrite them, therefore it suggests we shouldn't allow a PUSH PROMISE into the Cache. The counter-argument is that because it's Same-Origin, the SW 'could' be configured to allow or fetch /a.js. This creates an asymmetry in capabilities that's uncomfortable, but it does not seem to intrinsically create additional risk, so it's probably OK.
  • We can ignore the processing model for redirects, using the same logic as above - yes, it's an asymmetry, but the server 'could' be redirecting to the pushed resource as the final URL.
  • The cache mode follows the Request, so directly inserting a PUSH PROMISE into the cache meaningfully changes that process model. In this case, Step 20 of HTTP-network-or-cache-fetch. This is why we're exploring making PUSH PROMISE handling cache aware, to reduce the overhead involved in PUSH promises.

This is why I think that, generally speaking, we (Chrome folks) have mostly convinced ourselves that same-origin pushes to the cache are probably OK. However, there's a big looming question mark on whether it's the right thing for the user - when it's done right, it can reduce latency, but when it's done wrong, it can waste users' bandwidth and disk space. Further, as you've alluded to, from an implementation perspective, there's a lot of complexity - perhaps intractably so, given the API surface exposed - that doesn't have a lot of people jumping for joy at supporting either same-origin or cross-origin. An example of this is the intersection with the webRequest API and how that affects resource consumption - e.g. imagine an advertiser pushing assets to the client when they're running an ad-blocker.

The fact is, as far as implementations go, it is a different cache, and that's unlikely to change in the near future precisely because of the various complexities. So to the extent @annevk likes specs to reflect the real world, I'm wholly supportive of it. However, if other implementations are doing things differently, then it's something to explore and evaluate, but better to have the spec reflect truth.

@mnot
Copy link
Member

mnot commented Feb 27, 2017

@sleevi thanks for that.

The question of whether it's good for the user came up often during the development of H2, and IIRC was always answered by Chrome folks as "servers can already push things using inlining, so that's not a valid argument against Server Push." Forgive me if I enjoy the irony a bit here :)

It seems to me like server push adds complexity to webrequest regardless of how many caches you model this as, but of course I'm not as familiar with the details. I'd really like to hear from other implementers too.

@annevk
Copy link
Member Author

annevk commented Mar 15, 2023

Caches for the first two features in OP are defined thanks to @noamr and the last one might go the way of the dodo. And if not #51 can take care of it.

@annevk annevk closed this as completed Mar 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

9 participants