Skip to content

Commit 3d58e97

Browse files
committed
http2: improve error when server sends HTTP/1
If for some reason the server sends an HTTP/1.1 response 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". Signed-off-by: Oleg Zaytsev <[email protected]>
1 parent 511cc3a commit 3d58e97

File tree

3 files changed

+79
-0
lines changed

3 files changed

+79
-0
lines changed

http2/frame.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,16 @@ var fhBytes = sync.Pool{
225225
},
226226
}
227227

228+
var invalidHTTP1LookingFrameHeader = func() FrameHeader {
229+
fh, err := readFrameHeader(make([]byte, frameHeaderLen), strings.NewReader("HTTP/1.1 "))
230+
if err != nil {
231+
panic(err)
232+
}
233+
return fh
234+
}()
235+
236+
func (h FrameHeader) looksLikeHTTP1Header() bool { return h == invalidHTTP1LookingFrameHeader }
237+
228238
// ReadFrameHeader reads 9 bytes from r and returns a FrameHeader.
229239
// Most users should use Framer.ReadFrame instead.
230240
func ReadFrameHeader(r io.Reader) (FrameHeader, error) {
@@ -503,10 +513,16 @@ func (fr *Framer) ReadFrame() (Frame, error) {
503513
return nil, err
504514
}
505515
if fh.Length > fr.maxReadSize {
516+
if fh.looksLikeHTTP1Header() {
517+
return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", err)
518+
}
506519
return nil, ErrFrameTooLarge
507520
}
508521
payload := fr.getReadBuf(fh.Length)
509522
if _, err := io.ReadFull(fr.r, payload); err != nil {
523+
if fh.looksLikeHTTP1Header() {
524+
return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", err)
525+
}
510526
return nil, err
511527
}
512528
f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, fr.countError, payload)

http2/frame_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,27 @@ func TestReadFrameHeader(t *testing.T) {
626626
}
627627
}
628628

629+
func TestReadFrameHeader_FromHTTP1Header(t *testing.T) {
630+
tests := []struct {
631+
in string
632+
looksLikeHTTP1 bool
633+
}{
634+
// Ignore high bit:
635+
{in: "\xff\xff\xff" + "\xff" + "\xff" + "\xff\xff\xff\xff", looksLikeHTTP1: false},
636+
{in: "HTTP/1.1 400 Bad Request\r\n", looksLikeHTTP1: true},
637+
}
638+
for i, tt := range tests {
639+
got, err := readFrameHeader(make([]byte, 9), strings.NewReader(tt.in))
640+
if err != nil {
641+
t.Errorf("%d. readFrameHeader(%q) = %v", i, tt.in, err)
642+
continue
643+
}
644+
if got.looksLikeHTTP1Header() != tt.looksLikeHTTP1 {
645+
t.Errorf("%d. readFrameHeader(%q).looksLikeHTTP1Header = %v; want %v", i, tt.in, got.looksLikeHTTP1Header(), tt.looksLikeHTTP1)
646+
}
647+
}
648+
}
649+
629650
func TestReadWriteFrameHeader(t *testing.T) {
630651
tests := []struct {
631652
len uint32

http2/transport_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,48 @@ func TestTransport(t *testing.T) {
272272
}
273273
}
274274

275+
func TestTransportFailureErrorForHTTP1Response(t *testing.T) {
276+
const expectedHTTP1PayloadHint = "frame header looked like an HTTP/1.1 header"
277+
278+
ts := httptest.NewServer(http.NewServeMux())
279+
t.Cleanup(ts.Close)
280+
281+
for _, tc := range []struct {
282+
name string
283+
maxFrameSize uint32
284+
expectedErrorIs error
285+
}{
286+
{
287+
name: "with default max frame size",
288+
maxFrameSize: 0,
289+
},
290+
{
291+
name: "with enough frame size to start reading",
292+
maxFrameSize: invalidHTTP1LookingFrameHeader.Length + 1,
293+
},
294+
} {
295+
t.Run(tc.name, func(t *testing.T) {
296+
tr := &Transport{
297+
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
298+
return net.Dial(network, addr)
299+
},
300+
MaxReadFrameSize: tc.maxFrameSize,
301+
AllowHTTP: true,
302+
}
303+
304+
req, err := http.NewRequest("GET", ts.URL, nil)
305+
if err != nil {
306+
t.Fatal(err)
307+
}
308+
309+
_, err = tr.RoundTrip(req)
310+
if !strings.Contains(err.Error(), expectedHTTP1PayloadHint) {
311+
t.Errorf("expected error to contain %q, got %v", expectedHTTP1PayloadHint, err)
312+
}
313+
})
314+
}
315+
}
316+
275317
func testTransportReusesConns(t *testing.T, useClient, wantSame bool, modReq func(*http.Request)) {
276318
ts := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
277319
io.WriteString(w, r.RemoteAddr)

0 commit comments

Comments
 (0)