From 00a2a1adf207c47e105bf76ee793b78d14e68018 Mon Sep 17 00:00:00 2001 From: fhgffy <102001626+fhgffy@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:29:37 +0800 Subject: [PATCH] fix(http): preserve chunked request framing --- proxy/http/http.go | 4 +- proxy/http/server_test.go | 99 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 proxy/http/server_test.go diff --git a/proxy/http/http.go b/proxy/http/http.go index 565e91d..3de28e1 100644 --- a/proxy/http/http.go +++ b/proxy/http/http.go @@ -72,6 +72,9 @@ func parseStartLine(line string) (r1, r2, r3 string, ok bool) { } func cleanHeaders(header textproto.MIMEHeader) { + if len(header.Values("Transfer-Encoding")) > 0 { + header.Del("Content-Length") + } header.Del("Proxy-Connection") header.Del("Connection") header.Del("Keep-Alive") @@ -79,7 +82,6 @@ func cleanHeaders(header textproto.MIMEHeader) { header.Del("Proxy-Authorization") header.Del("TE") header.Del("Trailers") - header.Del("Transfer-Encoding") header.Del("Upgrade") } diff --git a/proxy/http/server_test.go b/proxy/http/server_test.go new file mode 100644 index 0000000..d07b6f4 --- /dev/null +++ b/proxy/http/server_test.go @@ -0,0 +1,99 @@ +package http + +import ( + "bufio" + "bytes" + "io" + "net" + stdhttp "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/nadoo/glider/proxy" +) + +type directTestProxy struct{} + +func (directTestProxy) Addr() string { return "direct" } +func (p directTestProxy) Dial(network, addr string) (net.Conn, proxy.Dialer, error) { + c, err := net.Dial(network, addr) + return c, &proxy.Direct{}, err +} +func (directTestProxy) DialUDP(string, string) (net.PacketConn, proxy.UDPDialer, error) { + return nil, &proxy.Direct{}, proxy.ErrNotSupported +} +func (directTestProxy) NextDialer(string) proxy.Dialer { return &proxy.Direct{} } +func (directTestProxy) Record(proxy.Dialer, bool) {} + +func TestServeForwardsChunkedRequestBody(t *testing.T) { + origin := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + stdhttp.Error(w, err.Error(), stdhttp.StatusBadRequest) + return + } + w.Write(body) + })) + defer origin.Close() + client, server := net.Pipe() + defer client.Close() + client.SetDeadline(time.Now().Add(3 * time.Second)) + done := make(chan struct{}) + go func() { + defer close(done) + s := &HTTP{proxy: directTestProxy{}} + s.Serve(server) + }() + request := "POST " + origin.URL + "/upload HTTP/1.1\r\n" + + "Host: " + strings.TrimPrefix(origin.URL, "http://") + "\r\n" + + "Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n" + + "5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n" + if _, err := io.WriteString(client, request); err != nil { + t.Fatal(err) + } + response, err := stdhttp.ReadResponse(bufio.NewReader(client), nil) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != stdhttp.StatusOK || string(body) != "hello world" { + t.Fatalf("upstream response = %d %q, want 200 hello world", response.StatusCode, body) + } + client.Close() + <-done +} + +func TestRequestFramingHeaders(t *testing.T) { + for _, tt := range []struct { + name string + headers string + transferCoding bool + contentLength bool + }{ + {"chunked", "Transfer-Encoding: chunked\r\n", true, false}, + {"chunked with length", "Transfer-Encoding: chunked\r\nContent-Length: 1\r\n", true, false}, + {"content length", "Content-Length: 11\r\n", false, true}, + } { + t.Run(tt.name, func(t *testing.T) { + raw := "POST http://example.com/upload HTTP/1.1\r\nHost: example.com\r\n" + tt.headers + "\r\n" + req, err := parseRequest(bufio.NewReader(strings.NewReader(raw))) + if err != nil { + t.Fatal(err) + } + var wire bytes.Buffer + req.WriteBuf(&wire) + if got := strings.Contains(wire.String(), "Transfer-Encoding:"); got != tt.transferCoding { + t.Errorf("Transfer-Encoding present = %v, want %v", got, tt.transferCoding) + } + if got := strings.Contains(wire.String(), "Content-Length:"); got != tt.contentLength { + t.Errorf("Content-Length present = %v, want %v", got, tt.contentLength) + } + }) + } +}