Overview

Since version 1.6, Go has transparently supported HTTP/2 for both clients and servers when using TLS 1.2. The support was made available via the golang.org/x/net/http2 library for Go 1.5.

What’s being documented here is a fork of golang.org/x/net/http2 (see below) which adds preliminary HTTP/2 Server Push support (for Go applications), tested in Google Chrome 49, 50, 52 and Firefox 46.

See also CloudFlare’s recent announcement for more information on HTTP/2 Server Push, as well as the standard’s information page: http://httpwg.org/specs/rfc7540.html#PushResources

A simple demonstration is available here: https://bradleyf.id.au:8443 and the source code is available here: https://github.com/bradleyfalzon/h2push-demo (fork is available: https://github.com/bradleyfalzon/net)

If you’re on Twitter and interested in IETF Internet-Drafts and Protocols, check out https://twitter.com/rfcbot

Usage

Clients are required to get the fork using go get github.com/bradleyfalzon/net/http2 and import accordingly import "github.com/bradleyfalzon/net/http2".

An appropriate HTTP/2 server can be created by using the following example:

s := &http.Server{
    Addr:           ":3003",
    Handler:        nil,
    ReadTimeout:    30 * time.Second,
    WriteTimeout:   30 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
http2.ConfigureServer(s, nil)

log.Fatal(s.ListenAndServeTLS("cert.pem", "key.pem"))

Within a http handler, to push a resource simply add the relative path of the resource using the Link header, for example:

w.Header().Add("Link", "</static/main.css>; rel=preload;")
w.Header().Add("Link", "</static/main.js>; rel=preload;")

Use Google Chrome 52 (currently a Canary release) to better view pushed resources (although earlier releases do support pushed resources, they just do not obviously indicate it). See the CloudFlare announcement for better information on viewing pushed resources.

  • Clients can push multiple resources
  • Resources are fetched by using the appropriate http handler, therefore static assets as well as dynamic assets can be pushed
  • In this implementation, a handler can detect if a resource was pushed by checking for presence of the h2push header. Note, this is not a trusted header, see discussion in implementation issues.
if _, ok := r.Header["H2push"]; ok {
    // Most likely a pushed request (but a client could have forged this header)
}

Implementation Details

You can quickly view the entire diff on GitHub.

HTTP/2 supports concurrent requests on a single TCP connection by multiplexing each request and response on their own stream.

HTTP/2 has different request and response types called frames. For example, a HEADERS frame contains either the request or response headers, a DATA frame contains the body for either request or response, and there are other frames that are unique to HTTP/2 such as SETTINGS, WINDOW_UPDATE and PUSH_PROMISE.

The HTTP/2 spec provides the implementation requirements and is available http://httpwg.org/specs/rfc7540.html, see the section on Server Push for specifics.

The first step is to detect which assets need to be pushed, this implementation detects resources to be pushed by the presence of a Link header when processing the response. This is a simple API, but this is (in this implementation at least) only detected after the initial response has been generated by the application. A server or proxy may immediately based on the request whether another resource should be pushed (such as the value or lack of a cookie, or if the resource is not cacheable), proxies may also wish to push resources before the initial response is ready. So future server implementations may chose other methods (discussed in closing notes).

Note, this implementation’s use of the Link header is semi-complete implementation of CloudFlare’s Server Push behaviour, whereby the application signals a resource to be pushed by setting a header containing the full path of the resource. A future implementation should either correctly follow the full Link draft (https://www.w3.org/Protocols/9707-link-header.html) or implement another API, with support for sending promises immediately without waiting for the request to finish processing.

Once the server knows of a resource to push, it must first send a PUSH_PROMISE frame to the client, before sending the response’s HEADER or DATA frame. See http://httpwg.org/specs/rfc7540.html#PushRequests for why this is the case.

There are rules on what type of resources can be pushed, and importantly, clients can disable server push; promises may be suitable for a web browser, but not for server to server communication, user-agents such as wget and curl, or potentially resource (bandwidth, CPU or battery) constrained devices.

These checks and detections can be seen: https://github.com/bradleyfalzon/net/commit/e5fbdb8434a6c8ca5b358cee38d2acb0070d8fb1#diff-51f54e5e768ac5a5b2539aebaf738475R1972

Once a resource has been chosen to be pushed, the PUSH_PROMISE frame needs to be created and sent on the existing stream. The frame contains a new stream ID which the resources will be sent on, as well as a mock of the request headers a client would have sent.

These mock headers gives the client an opportunity to reject the stream (by sending a RST_STREAM), potentially because the client already has the resource in its cache. Obviously some of the pushed resource may already have been sent, so it’s important to send the promise as early as possible, and only if it’s very likely the client doesn’t have the resource already, and the cost of addition network bandwidth (for both the client and server) doesn’t outweigh the performance gain.

Go’s /x/net/http2 didn’t originally contain the ability to create a stream, but by seeing how the existing processHeaders method creates a new stream from new requests, it’s possible to implement a limited (and incomplete) newStream method which sets up the internal state to support a new stream generated server-side. Future implementations will likely be able to remove some of the repetitive code.

Note, streams created by clients must be odd numbered (all streams start at stream ID 1, ID 0 is reserved for control frames), and servers create even numbered streams. New stream IDs must be greater than the last maximum. So a client creating a new stream with the ID 2^31-1 is probably going to have a bad time very quickly.

The newly created method responsible for building required frames, writePromise, modifies the http2.serverConn state - and it likely violates responsibility. See the TODO above the function definition for more information. This will likely cause issues but needs further investigation.

To send the PUSH_PROMISE frame a new writer struct was created in write.go which simply sends the frame.

This implementation’s generation of request headers only includes a minimal set of headers for the request to succeed (method, scheme, authority and path), it does not include Cookies, Accept etc. This is simply an implementation issue and will need to be fixed in production implementations.

Once the PUSH_PROMISE frame has been generated, and new set of request headers is generated (more repetition here) that will be sent to the http handler responsible for writing to the usual http.ResponseWriter - which in turn causes the response HEADER and DATA frames to be sent on the newly created stream ID.

The implementation currently available was designed to contain the minimum number of changes without refactoring the entire package - and production implementation will unlikely make this trade off as the developer will likely also have given themselves more than a weekend and already be familiar with the code.

To recap, the steps required are:

  • Detect assets to push
  • Ensure client has not disabled server push, and the assets can be sent
  • Create a new stream to send the response on
  • Create a fake set of request headers to send in PUSH_PROMISE
  • Send a PUSH_PROMISE frame on the existing stream and before the DATA of the initial request, this frame contains:
    • new stream ID which will be used to send the headers and data of the resource
    • faked request headers allowing the client to reject the resource (some data may already have been sent)
  • Call the appropriate http handler to write the response to the http.ResponseWriter
  • Sends response’s HEADER and DATA frames on the promised stream ID

Implementation Issues

  • This is not a complete nor correct implementation of HTTP/2 Server Push, nor is it suitable as a candidate for production implementation. Here be dragons.
  • A better implementation will likely focus on refactoring /x/net/http2 as well as possibly using the serve loop to listen for promises to be created (to safely mutate the http2.serverConn struct when creating streams).
  • This implementation does not handle fragmented headers, this is unlikely a problem in toy servers, but a requirement for a production implementation, however, it should currently send headers up to approximately 16K in size correctly.
  • This likely fails existing unit tests and does not include new nor does it update existing tests.
  • Doesn’t send promises for HEAD requests, this is by design as no testing has been done, but the HTTP/2 specification does not forbid it.
  • It currently adds a “h2push: true” header to pushed requests, this is only to enable the demo to differentiate pushed requests from standard requests, a production implementation would need to reconsider this approach as the headers are obviously easily forged by clients. The http.Request struct could be modified to include a Promised (or similar) bool field.
  • Only minimal http request headers are sent to the relevant http handlers, headers such as Cookie, User-Agent, Accept* etc are not current sent.
  • This does not check if any additional streams exceed the negotiated maximums.
  • This fork will not likely be updated from upstream nor maintained to a satisfactory level.
  • There maybe an issue with requesting the same pushed resource multiple times on the same TCP connection, more testing required.

Closing Notes

I would love to explore the possibility of implementing a cache based on HTTP/2 Server Push, either in a dedicated reverse proxy (similar to mod_pagespeed) or via a HTTP/2 Server such as Caddy via a plugin.

More investigation in exactly where Server Push is most beneficial needs to occur. HTTP/2 already provides great benefits via its multiplexing, perhaps pushed resources should only focus on pushing resources with the aim to reduce first paint events (style sheets) and to push critical scripts required for rendering.

Applications may need to ensure they don’t push resources in response to requests generated via XMLHttpRequest or similar methods.

The Link header method may not be the absolute best API for all applications, as mentioned already, it’s important for some promises to be sent very early, and using response header methods would require them to be processed before the request is finished processing or flushed. Response headers may provide a good method for intermediate proxies (which explains CloudFlare’s support).

Detecting when a resource is being push may also have it’s benefits (it’s not clear whether or how CloudFlare provides this).

Thanks

I would like to thank the Go authors for their initial implementation (you know who you are) and I look forward to a production implementation in later Go versions.

Thanks for SPDY team and those involved in the IETF WG for their work on HTTP/2 and supporting Server Push.

A big thanks to CloudFlare for giving me the idea for this weekend project, your open source contributions and staff are always inspirational.