Skip to content

Instantly share code, notes, and snippets.

@slightlyoff
Last active September 30, 2022 23:11
  • Star 23 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save slightlyoff/18dc42ae00768c23fbc4c5097400adfb to your computer and use it in GitHub Desktop.
Delivering H/2 Push Payloads To Userland

Background

One of the biggest missed opportunities thus far with HTTP/2 ("H/2") is that we are not yet able to sunset WebSockets in favor of H/2. Web Sockets and H/2 both support multiplexing messages bi-directionally and can send both textual and binary data.

Server Sent Events ("SSE"), by contrast, are not bi-directional (they're a "server-push-only" channel) and binary data cannot be sent easily. They are, however, very simple to implement. Adding to the menagerie of options, RTCPeerConnection can also be used to signal data to applications in a low-latency (but potentially lossy) way.

Because H/2 does not support the handshake (upgrade) that WebSockets use to negotiate a connection (abandoned draft approach to repairing here), web developers are faced with a complex set of choices regarding how to handle low-latency data updates for their apps.

Web apps may benefit from a WebSocket-like API for receiving data updates without a custom protocol (e.g., Web Sockets or SSE) and associated infrastructure. An API for H/2 push receipts seems like a natural solution to this quandry, but H/2 has created a non-origin sharing concept for pushed responses. The upshot is that H/2's behavior needs to be futher constrained to only consider single origins (or URLs) so that an application is not incidentially confused by pushes into a shared H/2 cache by an app for which the server is also authoritative but which the application does not wish to trust.

Proposal

The Server Sent Events API, perhaps, provides a way forward. What if, instead of only connecting to SSE-protocol supporting endpoints, a call to new EventSource(...) could also subscribe to pushed updates to individual URLs (or groups of URLs) and allow notifiation to the application whenever an H/2 PUSH_PROMISE resolves?

A minor extension to SSE might allow use of H/2 push as a substrate for delivering messages, with a slight modification of the event payload to accomodate providing Response objects as values:

// If an H/2 channel is open, any PUSH_PROMISE for the resource at
// `/endpoint` will trigger a "resourcepush" event
let es = new EventSource("/endpoint", { 
  withCredentials: true
});
es.addEventListener("resourcepush", (e) => {
  console.log(e.data instanceof Response);
});

Opting in to globbing for all push promises below a particular scope might look like:

// Any resource pushed to "/endpoint" or anything that matches that
// as a prefix will deliver to this handler
let es = new EventSource("/endpoint", { 
  withCredentials: true,
  matchScope: true
});
es.addEventListener("resourcepush", (e) => {
  console.log(e.data.url);
});

// If multiple handlers are installed, it's longest-prefix-wins, ala
// Service Workers:
let es = new EventSource("/endpoint/channel", { 
  withCredentials: true,
  matchScope: true
});
// If a push is sent to `/endpoint/blargity`, will be handled by previous source
// If a push is sent to `/endpoint/channel`, `/endpoint/channel1`, or 
// `/endpoint/channel2`, it will be delivered below:
es.addEventListener("resourcepush", (e) => {
  console.log("channel-pushed resource:", e.data.url);
});

Open questions

  • "resourcepush" is a terrible name.
  • should updates to previously pushed resources be opt-in instead of transparent?
  • do we need a version of this API that allows delivery to Service Workers?
  • how much of a problem is the H/2 vs. Origin Model split in practice? Do we actually know?
  • via Ryan Sleevi, to what extent should we allow subscriptions to any origin?

Alternatives Considered

A notable downside of the proposed design is that it might "miss" same-origin payloads sent earlier in the page's lifecycle over the channel. One could imagine a version of the proposal that adds a flag to enable subscription-time delivery of these resources so long as they're still in the push cache. Putting this feature as another optional flag feels clunky, though.

One way to make it less clunky would be to describe the API in terms of an Observer pattern (e.g. Intersection Observer). The above example might then become:

let options = {
  path: "/endpoint",
  withCredentials: true,
  matchScope: true
};
let callback = (records, observer) => {
  records.forEach((r) => {
    console.log(r instanceof Response); // === true
  };
};

let resoureObserver = new ResourceObserver(callback, options);

This feels somewhat better as one of the key distinguishers of Observer-pattern APIs is their ability to scope interest in a specific subset of all events internal to the system. We also dont need to cancel delivery, so phraising it as an Event might not be great. It is, however, much more new API surface area.

Another considered alternative might instead put registration for listening inside a Service Worker's installation or activation phase; e.g.:

self.addEventListener("install", (e) => {
  e.registerResourceListener("/endpoint", { 
    withCredentials: true,
    matchScope: true
  });
});

self.addEventListener("resourcePush", (e) => {
  console.log("channel-pushed resource:", e.data.url);
});

This is interesting because it would allow us to avoid delivering these events to all windows and tabs which are open, and doesn't upset our design goal for Service Workers to be shut down when not in use. The ergonomics suffer somewhat: what about pages that don't have a SW (e.g., first-load)? Does handling resource pushes then gate on SW install time (which can take a long while)? Also, Response objects can't be sent postMessage to clients as they aren't structured-clonable, meaning the SW would need to signal to a page that an update is available and then, perhaps, transfer the data by extracting it from the Response body to send via postMessage or transfer using the Cache API.

@LPardue
Copy link

LPardue commented Nov 28, 2017

I revisit this proposal with great interest, I gain more insight into the many facets each time.

I've spent some time recently looking at Server Push in HTTP/QUIC, it currently does away with notions of Stream ID and favours a Push ID. To that end, I'd encourage some care in assessing proposals such that any solution would naturally migrate to a future version of HTTP.

@jhampton
Copy link

@jakearchibald I found this thread and your article about browser support/implementation for HTTP/2 in the nick of time. I'm working on a project to implement a general-purpose fetch/XHR client that supports HTTP/2 push. I'm wondering if the needle has moved toward a standard since this thread began. I'll ping you on Twitter as well, and thank you very much for sharing your (hard-won) knowledge and thoughts with the community at large.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment