@marcel91200 / caddy Mirror · read-only / commits / 519be123
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 03:30 UTC

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

Jay Hemnani committed Oct 9, 2026 at 12:12 UTC 519be12306c5bfb325bd6bf9591ddbd4cdccff3e
2 files changed +64 -1
modules/caddyhttp/reverseproxy/headers_test.go
+60
@@ -2,7 +2,10 @@ package reverseproxy
2
3 import (
4 "context"
5 + "net/http"
6 "net/http/httptest"
7 + "net/http/httptrace"
8 + "net/textproto"
9 "testing"
10
11 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -125,3 +128,60 @@ func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
128 t.Errorf("X-Forwarded-Host = %q, want %q", got, "example.com")
129 }
130 }
131 +
132 +// TestInformationalResponseKeepsHandlerHeaders verifies that a 1xx response
133 +// from the upstream does not remove response headers set before proxying.
134 +func TestInformationalResponseKeepsHandlerHeaders(t *testing.T) {
135 + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136 + w.Header().Set("Link", "</style.css>; rel=preload")
137 + w.WriteHeader(http.StatusEarlyHints)
138 + w.Header().Del("Link")
139 + w.WriteHeader(http.StatusOK)
140 + }))
141 + t.Cleanup(backend.Close)
142 +
143 + h := minimalHandler(0, &Upstream{Host: new(Host), Dial: backend.Listener.Addr().String()})
144 +
145 + front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146 + w.Header().Set("Strict-Transport-Security", "max-age=31536000")
147 + _ = h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
148 + return nil
149 + }))
150 + }))
151 + t.Cleanup(front.Close)
152 +
153 + var earlyHints []textproto.MIMEHeader
154 + trace := &httptrace.ClientTrace{
155 + Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
156 + if code == http.StatusEarlyHints {
157 + earlyHints = append(earlyHints, header)
158 + }
159 + return nil
160 + },
161 + }
162 + req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, front.URL, nil)
163 + if err != nil {
164 + t.Fatal(err)
165 + }
166 + resp, err := http.DefaultClient.Do(req)
167 + if err != nil {
168 + t.Fatalf("request failed: %v", err)
169 + }
170 + resp.Body.Close()
171 +
172 + if len(earlyHints) != 1 {
173 + t.Fatalf("103 responses: got %d, want 1", len(earlyHints))
174 + }
175 + if got := earlyHints[0].Get("Link"); got != "</style.css>; rel=preload" {
176 + t.Errorf("Link on the 103: got %q, want %q", got, "</style.css>; rel=preload")
177 + }
178 + if resp.StatusCode != http.StatusOK {
179 + t.Fatalf("status: got %d, want %d", resp.StatusCode, http.StatusOK)
180 + }
181 + if got := resp.Header.Get("Strict-Transport-Security"); got != "max-age=31536000" {
182 + t.Errorf("Strict-Transport-Security: got %q, want %q", got, "max-age=31536000")
183 + }
184 + if got := resp.Header.Get("Link"); got != "" {
185 + t.Errorf("Link from the 103 leaked into the final response: %q", got)
186 + }
187 +}
modules/caddyhttp/reverseproxy/reverseproxy.go
+4 -1
@@ -23,6 +23,7 @@ import (
23 "errors"
24 "fmt"
25 "io"
26 + "maps"
27 "net"
28 "net/http"
29 "net/http/httptrace"
@@ -1174,12 +1175,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
1175 return nil
1176 }
1177 h := rw.Header()
1178 + ownHeaders := h.Clone()
1179 copyHeader(h, http.Header(header))
1180 rw.WriteHeader(code)
1181
1180 - // Clear headers coming from the backend
1182 + // Clear headers coming from the backend, keeping our own
1183 // (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses)
1184 clear(h)
1185 + maps.Copy(h, ownHeaders)
1186
1187 return nil
1188 },