I am trying to write a Go client to test our http/2 infrastructure. I would like to make an http request to https://mydomain.tld/somePage
and expect to receive an html response, along with several pushed resources. I would like to ensure those pushes are successful, and fail if they are not.
It is not clear to me if any part of the standard library exposes this functionality I need.
I can look at the response and check the protocol version to detect http2.
I can see Link
headers in responses from sites like https://http2-push.appspot.com/ that send pushes, but I'm not quite clear on the relationship between Link headers and actual Push Promise frames. You can get link headers over http 1.1, so I'm not sure that alone ensures a push will happen.
The http2 package has a lower level Framer
interface that I may be able to leverage to verify the raw frames, but honestly, I have no idea how to set one up and issue the initial request to it.
Is there any example of how a go client can verify the proper configuration of http2 pushed resources?
Unfortunately HTTP/2 push always felt like feature that wasn't quite there yet. It's usefulness was stunted due to Cache-Digest for HTTP/2 being killed off, and no browser APIs to hook into push events. The Chrome team has considered removing Push support since at least 2018.
In short, HTTP/2 breaks down the HTTP protocol communication into an exchange of binary-encoded frames, which are then mapped to messages that belong to a particular stream, all of which are multiplexed within a single TCP connection.
HTTP/2 Server Push allows an HTTP/2-compliant server to send resources to an HTTP/2-compliant client before the client requests them. Server Push is a performance technique aimed at reducing latency by loading resources preemptively, even before the client knows they will be needed.
Using the Framer in golang.org/x/net/http2 isn't hard, if we can get a copy of the bytes that are read naturally by the http.Client. We can do that by implementing our own net.Conn.
I made some progress with the program below, however I did not see the expected PUSH_PROMISE frames. After some digging around I found that the Go client explicitly disables Push. Servers are not allowed to send those frames in this case. I don't see an obvious way to change that setting (short of hacking the stdlib).
Thought I still share my code. Perhaps I missed something simple to make it work after all.
package main
import (
"bytes"
"crypto/tls"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"golang.org/x/net/http2"
)
func main() {
buf := &bytes.Buffer{}
transport := &http2.Transport{DialTLS: dialT(buf)}
client := &http.Client{Transport: transport}
res, err := client.Get("https://http2-push.appspot.com/")
if err != nil {
log.Fatal(err)
}
res.Body.Close()
res.Write(os.Stdout)
framer := http2.NewFramer(ioutil.Discard, buf)
for {
f, err := framer.ReadFrame()
if err == io.EOF || err == io.ErrUnexpectedEOF {
break
}
switch err.(type) {
case nil:
log.Println(f)
case http2.ConnectionError:
// Ignore. There will be many errors of type "PROTOCOL_ERROR, DATA
// frame with stream ID 0". Presumably we are abusing the framer.
default:
log.Println(err, framer.ErrorDetail())
}
}
}
// dialT returns a connection that writes everything that is read to w.
func dialT(w io.Writer) func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return func(network, addr string, cfg *tls.Config) (net.Conn, error) {
conn, err := tls.Dial(network, addr, cfg)
return &tConn{conn, w}, err
}
}
type tConn struct {
net.Conn
T io.Writer // receives everything that is read from Conn
}
func (w *tConn) Read(b []byte) (n int, err error) {
n, err = w.Conn.Read(b)
w.T.Write(b)
return
}
A patch was submitted for review.
"http2: support consuming PUSH_PROMISE streams in the client"
(The github issue has milestone "Unplanned", which hopefully won't give it significantly less priority in the review queue.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With