From 06dd7079a5f6f91809594574dd4938bcebcc0621 Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Sat, 31 Oct 2026 15:38:46 +0300 Subject: [PATCH] HTTP inbound: Forward successive informational responses Continue reading and forwarding interim responses until the final HTTP response, and treat 101 as terminal. Extend the existing response test with fragmented interim chains and verify the final body. Assisted-by: Codex Signed-off-by: Eljees <3.14hell@gmail.com> --- proxy/http/server.go | 51 +++++++++++++++++++----------------- proxy/http/server_test.go | 54 ++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 36 deletions(-) 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) } }