Skip to content

Commit 879ace1

Browse files
committed
net/http: keep Content-Encoding in Error, add GODEBUG for ServeContent
This reverts the changes to Error from CL 571995, and adds a GODEBUG controlling the changes to ServeContent/ServeFile/ServeFS. The change to remove the Content-Encoding header when serving an error breaks middleware which sets Content-Encoding: gzip and wraps a ResponseWriter in one which compresses the response body. This middleware already breaks when ServeContent handles a Range request. Correct uses of ServeContent which serve pre-compressed content with a Content-Encoding: gzip header break if we don't remove that header when serving errors. Therefore, we keep the change to ServeContent/ ServeFile/ServeFS, but we add the ability to disable the new behavior by setting GODEBUG=httpservecontentkeepheaders=1. We revert the change to Error, because users who don't want to include a Content-Encoding header in errors can simply remove the header themselves, or not add it in the first place. Fixes #66343 Change-Id: Ic19a24b73624a5ac1a258ed7a8fe7d9bf86c6a38 Reviewed-on: https://go-review.googlesource.com/c/go/+/593157 Reviewed-by: Russ Cox <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 2b12bbc commit 879ace1

File tree

8 files changed

+115
-19
lines changed

8 files changed

+115
-19
lines changed

doc/godebug.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,15 @@ This behavior is controlled by the `x509keypairleaf` setting. For Go 1.23, it
200200
defaults to `x509keypairleaf=1`. Previous versions default to
201201
`x509keypairleaf=0`.
202202

203+
Go 1.23 changed
204+
[`net/http.ServeContent`](/pkg/net/http#ServeContent),
205+
[`net/http.ServeFile`](/pkg/net/http#ServeFile), and
206+
[`net/http.ServeFS`](/pkg/net/http#ServeFS) to
207+
remove Cache-Control, Content-Encoding, Etag, and Last-Modified headers
208+
when serving an error. This behavior is controlled by
209+
the [`httpservecontentkeepheaders` setting](/pkg/net/http#ServeContent).
210+
Using `httpservecontentkeepheaders=1` restores the pre-Go 1.23 behavior.
211+
203212
### Go 1.22
204213

205214
Go 1.22 adds a configurable limit to control the maximum acceptable RSA key size
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
1-
[Error] now removes misleading response headers.
1+
[ServeContent], [ServeFile], and [ServeFileFS] now remove
2+
the `Cache-Control`, `Content-Encoding`, `Etag`, and `Last-Modified`
3+
headers when serving an error. These headers usually apply to the
4+
non-error content, but not to the text of errors.
5+
6+
Middleware which wraps a [ResponseWriter] and applies on-the-fly
7+
encoding, such as `Content-Encoding: gzip`, will not function after
8+
this change. The previous behavior of [ServeContent], [ServeFile],
9+
and [ServeFileFS] may be restored by setting
10+
`GODEBUG=httpservecontentkeepheaders=1`.
11+
12+
Note that middleware which changes the size of the served content
13+
(such as by compressing it) already does not function properly when
14+
[ServeContent] handles a Range request. On-the-fly compression
15+
should use the `Transfer-Encoding` header instead of `Content-Encoding`.
16+

src/internal/godebugs/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var All = []Info{
3636
{Name: "http2server", Package: "net/http"},
3737
{Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"},
3838
{Name: "httpmuxgo121", Package: "net/http", Changed: 22, Old: "1"},
39+
{Name: "httpservecontentkeepheaders", Package: "net/http", Changed: 23, Old: "0"},
3940
{Name: "installgoroot", Package: "go/build"},
4041
{Name: "jstmpllitinterp", Package: "html/template", Opaque: true}, // bug #66217: remove Opaque
4142
//{Name: "multipartfiles", Package: "mime/multipart"},

src/net/http/fs.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package http
99
import (
1010
"errors"
1111
"fmt"
12+
"internal/godebug"
1213
"io"
1314
"io/fs"
1415
"mime"
@@ -171,15 +172,37 @@ func dirList(w ResponseWriter, r *Request, f File) {
171172
fmt.Fprintf(w, "</pre>\n")
172173
}
173174

175+
// GODEBUG=httpservecontentkeepheaders=1 restores the pre-1.23 behavior of not deleting
176+
// Cache-Control, Content-Encoding, Etag, or Last-Modified headers on ServeContent errors.
177+
var httpservecontentkeepheaders = godebug.New("httpservecontentkeepheaders")
178+
174179
// serveError serves an error from ServeFile, ServeFileFS, and ServeContent.
175180
// Because those can all be configured by the caller by setting headers like
176181
// Etag, Last-Modified, and Cache-Control to send on a successful response,
177182
// the error path needs to clear them, since they may not be meant for errors.
178183
func serveError(w ResponseWriter, text string, code int) {
179184
h := w.Header()
180-
h.Del("Etag")
181-
h.Del("Last-Modified")
182-
h.Del("Cache-Control")
185+
186+
nonDefault := false
187+
for _, k := range []string{
188+
"Cache-Control",
189+
"Content-Encoding",
190+
"Etag",
191+
"Last-Modified",
192+
} {
193+
if !h.has(k) {
194+
continue
195+
}
196+
if httpservecontentkeepheaders.Value() == "1" {
197+
nonDefault = true
198+
} else {
199+
h.Del(k)
200+
}
201+
}
202+
if nonDefault {
203+
httpservecontentkeepheaders.IncNonDefault()
204+
}
205+
183206
Error(w, text, code)
184207
}
185208

@@ -203,11 +226,17 @@ func serveError(w ResponseWriter, text string, code int) {
203226
//
204227
// The content's Seek method must work: ServeContent uses
205228
// a seek to the end of the content to determine its size.
229+
// Note that [*os.File] implements the [io.ReadSeeker] interface.
206230
//
207231
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
208232
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
209233
//
210-
// Note that [*os.File] implements the [io.ReadSeeker] interface.
234+
// If an error occurs when serving the request (for example, when
235+
// handling an invalid range request), ServeContent responds with an
236+
// error message. By default, ServeContent strips the Cache-Control,
237+
// Content-Encoding, ETag, and Last-Modified headers from error responses.
238+
// The GODEBUG setting httpservecontentkeepheaders=1 causes ServeContent
239+
// to preserve these headers.
211240
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
212241
sizeFunc := func() (int64, error) {
213242
size, err := content.Seek(0, io.SeekEnd)

src/net/http/fs_test.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,8 +1223,20 @@ type issue12991File struct{ File }
12231223
func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
12241224
func (issue12991File) Close() error { return nil }
12251225

1226-
func TestFileServerErrorMessages(t *testing.T) { run(t, testFileServerErrorMessages) }
1227-
func testFileServerErrorMessages(t *testing.T, mode testMode) {
1226+
func TestFileServerErrorMessages(t *testing.T) {
1227+
run(t, func(t *testing.T, mode testMode) {
1228+
t.Run("keepheaders=0", func(t *testing.T) {
1229+
testFileServerErrorMessages(t, mode, false)
1230+
})
1231+
t.Run("keepheaders=1", func(t *testing.T) {
1232+
testFileServerErrorMessages(t, mode, true)
1233+
})
1234+
}, testNotParallel)
1235+
}
1236+
func testFileServerErrorMessages(t *testing.T, mode testMode, keepHeaders bool) {
1237+
if keepHeaders {
1238+
t.Setenv("GODEBUG", "httpservecontentkeepheaders=1")
1239+
}
12281240
fs := fakeFS{
12291241
"/500": &fakeFileInfo{
12301242
err: errors.New("random error"),
@@ -1254,8 +1266,12 @@ func testFileServerErrorMessages(t *testing.T, mode testMode) {
12541266
t.Errorf("GET /%d: StatusCode = %d; want %d", code, res.StatusCode, code)
12551267
}
12561268
for _, hdr := range []string{"Etag", "Last-Modified", "Cache-Control"} {
1257-
if v, ok := res.Header[hdr]; ok {
1258-
t.Errorf("GET /%d: Header[%q] = %q, want not present", code, hdr, v)
1269+
if v, got := res.Header[hdr]; got != keepHeaders {
1270+
want := "not present"
1271+
if keepHeaders {
1272+
want = "present"
1273+
}
1274+
t.Errorf("GET /%d: Header[%q] = %q, want %v", code, hdr, v, want)
12591275
}
12601276
}
12611277
}
@@ -1710,6 +1726,17 @@ func testFileServerDirWithRootFile(t *testing.T, mode testMode) {
17101726
}
17111727

17121728
func TestServeContentHeadersWithError(t *testing.T) {
1729+
t.Run("keepheaders=0", func(t *testing.T) {
1730+
testServeContentHeadersWithError(t, false)
1731+
})
1732+
t.Run("keepheaders=1", func(t *testing.T) {
1733+
testServeContentHeadersWithError(t, true)
1734+
})
1735+
}
1736+
func testServeContentHeadersWithError(t *testing.T, keepHeaders bool) {
1737+
if keepHeaders {
1738+
t.Setenv("GODEBUG", "httpservecontentkeepheaders=1")
1739+
}
17131740
contents := []byte("content")
17141741
ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
17151742
w.Header().Set("Content-Type", "application/octet-stream")
@@ -1738,6 +1765,12 @@ func TestServeContentHeadersWithError(t *testing.T) {
17381765
out, _ := io.ReadAll(res.Body)
17391766
res.Body.Close()
17401767

1768+
ifKept := func(s string) string {
1769+
if keepHeaders {
1770+
return s
1771+
}
1772+
return ""
1773+
}
17411774
if g, e := res.StatusCode, 416; g != e {
17421775
t.Errorf("got status = %d; want %d", g, e)
17431776
}
@@ -1750,16 +1783,16 @@ func TestServeContentHeadersWithError(t *testing.T) {
17501783
if g, e := res.Header.Get("Content-Length"), strconv.Itoa(len(out)); g != e {
17511784
t.Errorf("got content-length = %q, want %q", g, e)
17521785
}
1753-
if g, e := res.Header.Get("Content-Encoding"), ""; g != e {
1786+
if g, e := res.Header.Get("Content-Encoding"), ifKept("gzip"); g != e {
17541787
t.Errorf("got content-encoding = %q, want %q", g, e)
17551788
}
1756-
if g, e := res.Header.Get("Etag"), ""; g != e {
1789+
if g, e := res.Header.Get("Etag"), ifKept(`"abcdefgh"`); g != e {
17571790
t.Errorf("got etag = %q, want %q", g, e)
17581791
}
1759-
if g, e := res.Header.Get("Last-Modified"), ""; g != e {
1792+
if g, e := res.Header.Get("Last-Modified"), ifKept("Wed, 21 Oct 2015 07:28:00 GMT"); g != e {
17601793
t.Errorf("got last-modified = %q, want %q", g, e)
17611794
}
1762-
if g, e := res.Header.Get("Cache-Control"), ""; g != e {
1795+
if g, e := res.Header.Get("Cache-Control"), ifKept("immutable"); g != e {
17631796
t.Errorf("got cache-control = %q, want %q", g, e)
17641797
}
17651798
if g, e := res.Header.Get("Content-Range"), "bytes */7"; g != e {

src/net/http/serve_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7166,13 +7166,12 @@ func testErrorContentLength(t *testing.T, mode testMode) {
71667166
func TestError(t *testing.T) {
71677167
w := httptest.NewRecorder()
71687168
w.Header().Set("Content-Length", "1")
7169-
w.Header().Set("Content-Encoding", "ascii")
71707169
w.Header().Set("X-Content-Type-Options", "scratch and sniff")
71717170
w.Header().Set("Other", "foo")
71727171
Error(w, "oops", 432)
71737172

71747173
h := w.Header()
7175-
for _, hdr := range []string{"Content-Length", "Content-Encoding"} {
7174+
for _, hdr := range []string{"Content-Length"} {
71767175
if v, ok := h[hdr]; ok {
71777176
t.Errorf("%s: %q, want not present", hdr, v)
71787177
}

src/net/http/server.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,17 +2226,22 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
22262226
// writes are done to w.
22272227
// The error message should be plain text.
22282228
//
2229-
// Error deletes the Content-Length and Content-Encoding headers,
2229+
// Error deletes the Content-Length header,
22302230
// sets Content-Type to “text/plain; charset=utf-8”,
22312231
// and sets X-Content-Type-Options to “nosniff”.
22322232
// This configures the header properly for the error message,
22332233
// in case the caller had set it up expecting a successful output.
22342234
func Error(w ResponseWriter, error string, code int) {
22352235
h := w.Header()
2236-
// We delete headers which might be valid for some other content,
2237-
// but not anymore for the error content.
2236+
2237+
// Delete the Content-Length header, which might be for some other content.
2238+
// Assuming the error string fits in the writer's buffer, we'll figure
2239+
// out the correct Content-Length for it later.
2240+
//
2241+
// We don't delete Content-Encoding, because some middleware sets
2242+
// Content-Encoding: gzip and wraps the ResponseWriter to compress on-the-fly.
2243+
// See https://go.dev/issue/66343.
22382244
h.Del("Content-Length")
2239-
h.Del("Content-Encoding")
22402245

22412246
// There might be content type already set, but we reset it to
22422247
// text/plain for the error message.

src/runtime/metrics/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ Below is the full list of supported metrics, ordered lexicographically.
267267
The number of non-default behaviors executed by the net/http
268268
package due to a non-default GODEBUG=httpmuxgo121=... setting.
269269
270+
/godebug/non-default-behavior/httpservecontentkeepheaders:events
271+
The number of non-default behaviors executed
272+
by the net/http package due to a non-default
273+
GODEBUG=httpservecontentkeepheaders=... setting.
274+
270275
/godebug/non-default-behavior/installgoroot:events
271276
The number of non-default behaviors executed by the go/build
272277
package due to a non-default GODEBUG=installgoroot=... setting.

0 commit comments

Comments
 (0)