Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I read HTTP/2 push frames from a net/http request

Tags:

go

http2

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?

like image 482
captncraig Avatar asked May 08 '17 16:05

captncraig


People also ask

Can I use http2 push?

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.

What is an http2 frame?

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.

How does http2 server push work?

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.


2 Answers

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
}
like image 166
Peter Avatar answered Oct 18 '22 01:10

Peter


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.)

like image 28
lf215 Avatar answered Oct 18 '22 03:10

lf215