diff --git a/proxy/http/server.go b/proxy/http/server.go index 349ba7dde647..abfa400e0940 100644 --- a/proxy/http/server.go +++ b/proxy/http/server.go @@ -312,34 +312,37 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri // Sometimes, server might send 1xx response to client // it should not be processed by http proxy handler, just forward it to client func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer io.Writer) (*http.Response, error) { - // Wait for the HTTP/1.x status prefix before inspecting the buffered response. - r.Peek(12) - peekBytes, err := r.Peek(min(56, r.Buffered())) - if err == nil || err == bufio.ErrBufferFull { - str := string(peekBytes) - ResponseLine := strings.Split(str, "\r\n")[0] - _, status, _ := strings.Cut(ResponseLine, " ") - // only handle 1xx response - if strings.HasPrefix(status, "1") { - ResponseHeader1xx := []byte{} - // read until \r\n\r\n (end of http response header) - for { - data, err := r.ReadSlice('\n') - if err != nil { - return nil, errors.New("failed to read http 1xx response").Base(err) - } - ResponseHeader1xx = append(ResponseHeader1xx, data...) - if len(ResponseHeader1xx) >= 4 && bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) { - break - } - if len(ResponseHeader1xx) > 1024 { - return nil, errors.New("too big http 1xx response") + for { + // Wait for the HTTP/1.x status prefix before inspecting the buffered response. + r.Peek(12) + peekBytes, err := r.Peek(min(56, r.Buffered())) + if err == nil || err == bufio.ErrBufferFull { + str := string(peekBytes) + ResponseLine := strings.Split(str, "\r\n")[0] + _, status, _ := strings.Cut(ResponseLine, " ") + // only handle 1xx response + if strings.HasPrefix(status, "1") && !strings.HasPrefix(status, "101") { + ResponseHeader1xx := []byte{} + // read until \r\n\r\n (end of http response header) + for { + data, err := r.ReadSlice('\n') + if err != nil { + return nil, errors.New("failed to read http 1xx response").Base(err) + } + ResponseHeader1xx = append(ResponseHeader1xx, data...) + if len(ResponseHeader1xx) >= 4 && bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) { + break + } + if len(ResponseHeader1xx) > 1024 { + return nil, errors.New("too big http 1xx response") + } } + writer.Write(ResponseHeader1xx) + continue } - writer.Write(ResponseHeader1xx) } + return http.ReadResponse(r, req) } - return http.ReadResponse(r, req) } func init() { diff --git a/proxy/http/server_test.go b/proxy/http/server_test.go index 5b3563315de8..4a2dc972f3b2 100644 --- a/proxy/http/server_test.go +++ b/proxy/http/server_test.go @@ -28,23 +28,53 @@ func TestReadResponseAndHandle100ContinueDoesNotPanicOnEarlyNewline(t *testing.T } func TestReadResponseAndHandle100ContinueForwardsAndParsesFinalResponse(t *testing.T) { - payload := "HTTP/1.1 100 Continue\r\n\r\n" + - "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello" - r := bufio.NewReader(iotest.OneByteReader(bytes.NewReader([]byte(payload)))) - req, err := http.NewRequest("GET", "http://example.com/", nil) - if err != nil { - t.Fatal(err) + const cont = "HTTP/1.1 100 Continue\r\n\r\n" + const hint = "HTTP/1.1 103 Early Hints\r\nLink: ; rel=preload; as=style\r\n\r\n" + for _, tc := range []struct{ name, interim string }{ + {"no_interim", ""}, + {"continue", cont}, + {"early_hints", hint}, + {"multiple_hints", hint + hint}, + {"continue_and_hints", cont + hint}, + } { + t.Run(tc.name, func(t *testing.T) { + payload := tc.interim + "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello" + r := bufio.NewReader(iotest.OneByteReader(strings.NewReader(payload))) + req, err := http.NewRequest("GET", "http://example.com/", nil) + if err != nil { + t.Fatal(err) + } + var forwarded bytes.Buffer + resp, err := readResponseAndHandle100Continue(r, req, &forwarded) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 200 || string(body) != "hello" || forwarded.String() != tc.interim { + t.Fatalf("status=%d body=%q forwarded=%q; want 200, hello, %q", resp.StatusCode, body, forwarded.String(), tc.interim) + } + }) } +} +func TestReadResponseAndHandle100ContinueStopsAtSwitchingProtocols(t *testing.T) { + const upgrade = "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: example\r\n\r\n" + r := bufio.NewReader(iotest.OneByteReader(strings.NewReader(upgrade + "opaque"))) var forwarded bytes.Buffer - resp, err := readResponseAndHandle100Continue(r, req, &forwarded) + resp, err := readResponseAndHandle100Continue(r, &http.Request{Method: "GET"}, &forwarded) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatal(err) } - if resp.StatusCode != 200 { - t.Fatalf("expected status 200, got %d", resp.StatusCode) + defer resp.Body.Close() + remaining, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) } - if !strings.Contains(forwarded.String(), "100 Continue") { - t.Fatalf("expected 1xx response to be forwarded, got %q", forwarded.String()) + if resp.StatusCode != 101 || forwarded.Len() != 0 || string(remaining) != "opaque" { + t.Fatalf("status=%d forwarded=%q remaining=%q", resp.StatusCode, forwarded.String(), remaining) } }