Samuel Williams Sunday, 10 February 2019

As web standards continue to evolve, Rack has sometimes struggled to provide appropriate interfaces. HTTP/2 push promises expand on the request-response model of traditional web applications, and requires explicit support from both servers and applications. We present Falcon, which implements HTTP/2 server push and discuss the strengths and weaknesses of the current rack.early_hints proposal.

HTTP/2 Push Promises

A normal interaction between a client and server involves the client sending a request to the server, and the server sending a response back to the client.

A push promise is very similar, except instead of the client generating the request, the server generates the request (push promise), and sends it to the client, along with the response. By initiating the requests this way, overall latency can be greatly reduced.

Early Hints

Early hints, as proposed, include link headers (as defined by RFC8288) that inform the client that additional resources will likely be needed. In HTTP/1 this is done using a special informational response:

Client Request:

	GET / HTTP/1.1
	Host: example.com

Server Response:

	HTTP/1.1 103 Early Hints
	Link: </style.css>; rel=preload; as=style
	Link: </script.js>; rel=preload; as=script

	HTTP/1.1 200 OK
	Date: Fri, 26 May 2017 10:02:11 GMT
	Content-Length: 1234
	Content-Type: text/html; charset=utf-8
	Link: </style.css>; rel=preload; as=style
	Link: </script.js>; rel=preload; as=script

	<!doctype html>
	[... rest of the response body is omitted from the example ...]

At the present time, no major browser supports the 103 early hints; It is primarily used between backend application servers that only talk HTTP/1, and front-end proxies that support HTTP/2. The early hints can be used to drive HTTP/2 push promises in such a configuration.

Falcon and Push Promises

As Falcon directly supports HTTP/2, early hints are converted into push promises within the server. This can reduce round-trip latency significantly compared to HTTP/1 early hints.

In rack, you can supply early hints using the proposed interface:

class EarlyHints
	def initialize(app)
		@app = app
	end
	
	def call(env)
		if env['PATH_INFO'] == "/index.html" and early_hints = env['rack.early_hints']
			early_hints.call([
				["link", "</style.css>; rel=preload; as=style"],
				["link", "</script.js>; rel=preload; as=script"],
			])
		end
		
		@app.call(env)
	end
end

It is possible to see the push promises using the Chrome network inspector:

Limitations

There are several HTTP/1 centric features of the rack interface. Firstly, it's possible to send any arbitrary header via early hints. In the case of HTTP/2, it is less clear how this should work since the primary use case is to generate push promises.

Because these are arbitrary headers, Falcon has to parse them to generate the push promises. This is less robust than invoking a function with appropriate arguments.

Finally, one has to wonder if push promises should be a concern of the front-end proxy rather than the application server. It should be possible to look at what resources are often sent together and intelligently generate push promises on a per-client basis. Should the application server be responsible for this?

Stepping Back

Many existing application servers are limited to HTTP/1 semantics. The trend seems to be to expand HTTP/1 with semantics to match the capabilities of HTTP/2, so that HTTP/2 can be fully utilized by whatever proxy is fronting the application servers. This is a common situation with Nginx+Puma or Nginx+Passenger, because Nginx lacks the ability, at the present time, to proxy requests using HTTP/2.

Rather than modifying the HTTP/1 protocol with informational responses and related orthogonal semantics, which sometimes feel like a hack, perhaps a better solution is to start building application servers capable of serving HTTP/2 directly. If that's the direction forward, ideally the interfaces built into rack don't make too many assumptions about the underlying protocol.

A Complementary Proposal

As well as the raw links interface, it would be nice to support a method which takes a path and related options, semantically equivalent to the link header:

def call(env)
	if early_hints = env['rack.early_hints']
		early_hints.push("/style.css", preload: true, rel: 'style')
		early_hints.push("/script.js", rel: 'script')
	end
end

This interface is less protocol specific, and can map cleanly to the underlying semantics of HTTP/1 and HTTP/2. This interface is also supported in the latest Falcon release.

Conclusion

Early hints allow Falcon to send push promises when serving a client using HTTP/2. This allows resources to be more efficiently delivered to the client. Many existing application servers only support HTTP/1 which need to use an informational response, which is currently not well supported by clients. Falcon implements early hints using push promises, and thus can serve these directly to clients.

Comments

Rusty World Needs a True Otaku. Please come to Rust-Lang and bring honor to Rustaceans.

Leave a comment

Please note, comments must be formatted using Markdown. Links can be enclosed in angle brackets, e.g. <www.codeotaku.com>.