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
},