How to use Golang 1.8’s HTTP/2 Server Push API

Golang 1.8 was released with HTTP/2 server push feature. Golang updated net/http package to support HTTP/2 feature at 1.6 and that code already supported PUSH_PROMISE frame that is used for server push, but 1.6 didn’t have API to spawn that frame. And then, 1.8 contains that API finally.


How to Use Server Push?

http.ResponseWriter has an unique characteristic. The structure behind http.ResponseWriter interface has hidden locked methods, and they were unlocked via type casting. Many go programmers familiar with http.ResponseWriter, but only one tenth of them knows that (I researched at Golang 1.8 release party at Tokyo).

Golang 1.7 and lower already has three hidden interfaces:

func handler(w http.ResponseWriter, r *http.Request) {
// Unlock chunked response feature of HTTP/1.1
f, ok := w.(http.Flusher)
if ok {
f.Flush()
}

// Unlock helper function to support chunked long polling(Server-Sent Events)
c, ok := w.(http.CloseNotifier)
if ok {
go func() {
for {
select {
case <-c.CloseNotify():

}
}
}()
}

// Unlock low level socket access to support WebSocket
h, ok := w.(http.Hijacker)
if ok {
conn, rw, err := h.Hijack()
}
}

Golang 1.8 implements Push API like them:

func handler(w http.ResponseWriter, r *http.Request) {
// Unlock HTTP/2 server push
p, ok := w.(http.Pusher)
if ok {
p.Push("/cool_style.css", nil)
}
}

You can add HTTP/2 server push feature by just adding the above code inside http handler functions. You don’t have to add any special configuration outside of the function.

If user agents that don’t support HTTP/2, casting to http.Pusher fails (ok become false), and Push() method calls are omitted. You can simply test with GODEBUG=http2server=0 (disable HTTP2 server feature).


How it Works?

The following code is a complete example to use server push feature:

package main
import (
"fmt"
"io/ioutil"
"net/http"
)
var image []byte
// preparing image
func init() {
var err error
image, err = ioutil.ReadFile("./image.png")
if err != nil {
panic(err)
}
}
// Send HTML and push image
func handlerHtml(w http.ResponseWriter, r *http.Request) {
pusher, ok := w.(http.Pusher)
if ok {
fmt.Println("Push /image")
pusher.Push("/image", nil)
}
w.Header().Add("Content-Type", "text/html")
fmt.Fprintf(w, `<html><body><img src="/image"></body></html>`)
}
// Send image as usual HTTP request
func handlerImage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.Write(image)
}
func main() {
http.HandleFunc("/", handlerHtml)
http.HandleFunc("/image", handlerImage)
fmt.Println("start http listening :18443")
err := http.ListenAndServeTLS(":18443", "server.crt", "server.key", nil)
fmt.Println(err)
}

After Push() method is called, net/http package creates pseudo HTTP requests inside HTTP servers. handlerImage() function is called with the pseudo request. You don’t have to implement any extra code to push resources. net/http reuse existing handler functions for server push.

Do you want to detect pseudo request? You can do it by checking r.Header.Get('User-Agent'). Pseudo requests only have Host header that is required by RFC. I have never seen any user agents that don’t send User-Agent header. You can push different content with regular HTTP request even though that is meaning less.


Concern about Performance

You can see the improvement of server push via developer tools of browsers. Chrome shows detail report of network.

The following screenshot is a result of HTTP/1.1:

This is the result of HTTP/2:

I have one potential concern about performance. First one is the timing actually the push performs. I tried to check behavior of Golang’s implementation and notified that:

  • Server push is performed after handlerHtml() function is completed.
  • Even if chunked response (http.Flusher) is used, the push is performed when the session is closed.

If HTML files would be generated with content of RDB and took time so long, it would be a perfect timing to use server push. But I can’t do that with current implementation.


Bug?

I tried to use second parameter of Push() method, but the headers I added were ignored (like Cache-Control). Only headers that was added in handlerImage() work collectly. I will check the net/http code after tex return submission.