reverseproxy: Keep own headers after 1xx responses (#8169)

* reverseproxy: Keep own headers after 1xx responses

Since #6656 the 1xx handler clears the whole header map after writing the
informational response, which also drops headers set before proxying (for
example by the header handler). Restore those after clearing, as #5091 did.

* reverseproxy: Use maps.Copy to restore own headers

* reverseproxy: Check the 103 reaches the client in the 1xx headers test
pull/8186/head
Jay Hemnani 2026-10-09 12:12:26 +05:30 committed by GitHub
parent 1b3838c1fd
commit 519be12306
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 1 deletions

View File

@ -2,7 +2,10 @@ package reverseproxy
import (
"context"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"net/textproto"
"testing"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@ -125,3 +128,60 @@ func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
t.Errorf("X-Forwarded-Host = %q, want %q", got, "example.com")
}
}
// TestInformationalResponseKeepsHandlerHeaders verifies that a 1xx response
// from the upstream does not remove response headers set before proxying.
func TestInformationalResponseKeepsHandlerHeaders(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Link", "</style.css>; rel=preload")
w.WriteHeader(http.StatusEarlyHints)
w.Header().Del("Link")
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(backend.Close)
h := minimalHandler(0, &Upstream{Host: new(Host), Dial: backend.Listener.Addr().String()})
front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
_ = h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
return nil
}))
}))
t.Cleanup(front.Close)
var earlyHints []textproto.MIMEHeader
trace := &httptrace.ClientTrace{
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
if code == http.StatusEarlyHints {
earlyHints = append(earlyHints, header)
}
return nil
},
}
req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, front.URL, nil)
if err != nil {
t.Fatal(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
resp.Body.Close()
if len(earlyHints) != 1 {
t.Fatalf("103 responses: got %d, want 1", len(earlyHints))
}
if got := earlyHints[0].Get("Link"); got != "</style.css>; rel=preload" {
t.Errorf("Link on the 103: got %q, want %q", got, "</style.css>; rel=preload")
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("status: got %d, want %d", resp.StatusCode, http.StatusOK)
}
if got := resp.Header.Get("Strict-Transport-Security"); got != "max-age=31536000" {
t.Errorf("Strict-Transport-Security: got %q, want %q", got, "max-age=31536000")
}
if got := resp.Header.Get("Link"); got != "" {
t.Errorf("Link from the 103 leaked into the final response: %q", got)
}
}

View File

@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"io"
"maps"
"net"
"net/http"
"net/http/httptrace"
@ -1174,12 +1175,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
return nil
}
h := rw.Header()
ownHeaders := h.Clone()
copyHeader(h, http.Header(header))
rw.WriteHeader(code)
// Clear headers coming from the backend
// Clear headers coming from the backend, keeping our own
// (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses)
clear(h)
maps.Copy(h, ownHeaders)
return nil
},