http2: improve error when server sends HTTP/1
If for some reason the server sends an HTTP/1.1 response
starting with "HTTP/1.1 " (9 bytes), http2 transport interprets that as
a valid frame header for an expected payload of ~4MB, and fails with
non-descriptive messages like "unexpected EOF".
This could happen, for example, if ALPN is misconfigured on the server's
load balancer.
This change attempts to improve that feedback by noting in the error
messages whenever the frame header was exactly the "HTTP/1.1 " bytes.
Change-Id: I7bf9ed2ee7f299b939b9004386f5bfa30a4e9032
GitHub-Last-Rev: d6e410daa3a1aa5d1c85ff99c929d27b81cf4783
GitHub-Pull-Request: golang/net#224
Reviewed-on: https://go-review.googlesource.com/c/net/+/623155
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Auto-Submit: Sean Liao <sean@liao.dev>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Sean Liao <sean@liao.dev>
diff --git a/http2/frame.go b/http2/frame.go
index 81faec7..97bd8b0 100644
--- a/http2/frame.go
+++ b/http2/frame.go
@@ -225,6 +225,11 @@
},
}
+func invalidHTTP1LookingFrameHeader() FrameHeader {
+ fh, _ := readFrameHeader(make([]byte, frameHeaderLen), strings.NewReader("HTTP/1.1 "))
+ return fh
+}
+
// ReadFrameHeader reads 9 bytes from r and returns a FrameHeader.
// Most users should use Framer.ReadFrame instead.
func ReadFrameHeader(r io.Reader) (FrameHeader, error) {
@@ -503,10 +508,16 @@
return nil, err
}
if fh.Length > fr.maxReadSize {
+ if fh == invalidHTTP1LookingFrameHeader() {
+ return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", err)
+ }
return nil, ErrFrameTooLarge
}
payload := fr.getReadBuf(fh.Length)
if _, err := io.ReadFull(fr.r, payload); err != nil {
+ if fh == invalidHTTP1LookingFrameHeader() {
+ return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", err)
+ }
return nil, err
}
f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, fr.countError, payload)
diff --git a/http2/transport_test.go b/http2/transport_test.go
index 1eeb76e..596499f 100644
--- a/http2/transport_test.go
+++ b/http2/transport_test.go
@@ -272,6 +272,48 @@
}
}
+func TestTransportFailureErrorForHTTP1Response(t *testing.T) {
+ const expectedHTTP1PayloadHint = "frame header looked like an HTTP/1.1 header"
+
+ ts := httptest.NewServer(http.NewServeMux())
+ t.Cleanup(ts.Close)
+
+ for _, tc := range []struct {
+ name string
+ maxFrameSize uint32
+ expectedErrorIs error
+ }{
+ {
+ name: "with default max frame size",
+ maxFrameSize: 0,
+ },
+ {
+ name: "with enough frame size to start reading",
+ maxFrameSize: invalidHTTP1LookingFrameHeader().Length + 1,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ tr := &Transport{
+ DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
+ return net.Dial(network, addr)
+ },
+ MaxReadFrameSize: tc.maxFrameSize,
+ AllowHTTP: true,
+ }
+
+ req, err := http.NewRequest("GET", ts.URL, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = tr.RoundTrip(req)
+ if !strings.Contains(err.Error(), expectedHTTP1PayloadHint) {
+ t.Errorf("expected error to contain %q, got %v", expectedHTTP1PayloadHint, err)
+ }
+ })
+ }
+}
+
func testTransportReusesConns(t *testing.T, useClient, wantSame bool, modReq func(*http.Request)) {
ts := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, r.RemoteAddr)