diff --git a/doc/next/1-intro.md b/doc/next/1-intro.md new file mode 100644 index 00000000000000..709ddf58378e5e --- /dev/null +++ b/doc/next/1-intro.md @@ -0,0 +1,8 @@ + + +## DRAFT RELEASE NOTES — Introduction to Go 1.28 {#introduction} + +**Go 1.28 is not yet released. These are work-in-progress release notes. +Go 1.28 is expected to be released in February 2027.** diff --git a/doc/next/2-language.md b/doc/next/2-language.md new file mode 100644 index 00000000000000..61030bd67606b0 --- /dev/null +++ b/doc/next/2-language.md @@ -0,0 +1,3 @@ +## Changes to the language {#language} + + diff --git a/doc/next/3-tools.md b/doc/next/3-tools.md new file mode 100644 index 00000000000000..5638f240a5b127 --- /dev/null +++ b/doc/next/3-tools.md @@ -0,0 +1,6 @@ +## Tools {#tools} + +### Go command {#go-command} + +### Cgo {#cgo} + diff --git a/doc/next/4-runtime.md b/doc/next/4-runtime.md new file mode 100644 index 00000000000000..1f8e445e0b10de --- /dev/null +++ b/doc/next/4-runtime.md @@ -0,0 +1 @@ +## Runtime {#runtime} diff --git a/doc/next/5-toolchain.md b/doc/next/5-toolchain.md new file mode 100644 index 00000000000000..0f4a816479754c --- /dev/null +++ b/doc/next/5-toolchain.md @@ -0,0 +1,7 @@ +## Compiler {#compiler} + +## Assembler {#assembler} + +## Linker {#linker} + + diff --git a/doc/next/6-stdlib/0-heading.md b/doc/next/6-stdlib/0-heading.md new file mode 100644 index 00000000000000..a992170d433326 --- /dev/null +++ b/doc/next/6-stdlib/0-heading.md @@ -0,0 +1,2 @@ +## Standard library {#library} + diff --git a/doc/next/6-stdlib/99-minor/0-heading.md b/doc/next/6-stdlib/99-minor/0-heading.md new file mode 100644 index 00000000000000..266d98f496a779 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/0-heading.md @@ -0,0 +1 @@ +### Minor changes to the library {#minor_library_changes} diff --git a/doc/next/6-stdlib/99-minor/README b/doc/next/6-stdlib/99-minor/README new file mode 100644 index 00000000000000..fac778de050642 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/README @@ -0,0 +1 @@ +API changes and other small changes to the standard library go here. diff --git a/doc/next/7-ports.md b/doc/next/7-ports.md new file mode 100644 index 00000000000000..8bea3f8fbc33f9 --- /dev/null +++ b/doc/next/7-ports.md @@ -0,0 +1,2 @@ +## Ports {#ports} + diff --git a/src/cmd/go.mod b/src/cmd/go.mod index ebe4c80adbbdce..9aa7c87ac0e0cf 100644 --- a/src/cmd/go.mod +++ b/src/cmd/go.mod @@ -11,7 +11,7 @@ require ( golang.org/x/sys v0.45.0 golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 golang.org/x/term v0.43.0 - golang.org/x/tools v0.45.1-0.20260710204130-3cdf838841de + golang.org/x/tools v0.45.1-0.20260714184632-28657d66a087 ) require ( diff --git a/src/cmd/go.sum b/src/cmd/go.sum index 026005eff21a99..9c6aa59dc288d6 100644 --- a/src/cmd/go.sum +++ b/src/cmd/go.sum @@ -22,7 +22,7 @@ golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/tools v0.45.1-0.20260710204130-3cdf838841de h1:DAFpdvgVicTKCGgv6rwHZv1Nk1fRPM1r/yyTbH9XbaA= -golang.org/x/tools v0.45.1-0.20260710204130-3cdf838841de/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.45.1-0.20260714184632-28657d66a087 h1:OBacbO7Pun0pcIzRzkngMhqkKWPEgHQP6EZmltKDw4o= +golang.org/x/tools v0.45.1-0.20260714184632-28657d66a087/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef h1:mqLYrXCXYEZOop9/Dbo6RPX11539nwiCNBb1icVPmw8= rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef/go.mod h1:8xcPgWmwlZONN1D9bjxtHEjrUtSEa3fakVF8iaewYKQ= diff --git a/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/stdversion/stdversion.go b/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/stdversion/stdversion.go index d1fda880e847a4..3d44c4e86da6f9 100644 --- a/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/stdversion/stdversion.go +++ b/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/stdversion/stdversion.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" ) @@ -64,8 +65,8 @@ func run(pass *analysis.Pass) (any, error) { pkg *types.Package version string } - memo := make(map[key]map[types.Object]string) // records symbol's minimum Go version - disallowedSymbols := func(pkg *types.Package, version string) map[types.Object]string { + memo := make(map[key]map[types.Object]stdlib.Symbol) + disallowedSymbols := func(pkg *types.Package, version string) map[types.Object]stdlib.Symbol { k := key{pkg, version} disallowed, ok := memo[k] if !ok { @@ -98,20 +99,12 @@ func run(pass *analysis.Pass) (any, error) { if fileVersion != "" { if obj, ok := pass.TypesInfo.Uses[n]; ok && obj.Pkg() != nil { disallowed := disallowedSymbols(obj.Pkg(), fileVersion) - if minVersion, ok := disallowed[origin(obj)]; ok { - // Some symbols are accessible before their release but - // only with specific build tags unknown to us here. - // Avoid false positives in such cases. - // TODO(mkalil): move this check into typesinternal.TooNewStdSymbols. - if obj.Pkg().Path() == "testing/synctest" && versions.AtLeast(fileVersion, "go1.24") { - break // requires go1.24 && goexperiment.synctest || go1.25 - } - noun := "module" - if fileVersion != pkgVersion { - noun = "file" - } + if sym, ok := disallowed[origin(obj)]; ok { pass.ReportRangef(n, "%s.%s requires %v or later (%s is %s)", - obj.Pkg().Name(), obj.Name(), minVersion, noun, fileVersion) + obj.Pkg().Name(), sym.Name, + sym.Version, + cond(fileVersion != pkgVersion, "file", "module"), + fileVersion) } } } @@ -134,3 +127,11 @@ func origin(obj types.Object) types.Object { } return obj } + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +} diff --git a/src/cmd/vendor/golang.org/x/tools/internal/typesinternal/toonew.go b/src/cmd/vendor/golang.org/x/tools/internal/typesinternal/toonew.go index cc86487eaa0a07..9cc58fd146566f 100644 --- a/src/cmd/vendor/golang.org/x/tools/internal/typesinternal/toonew.go +++ b/src/cmd/vendor/golang.org/x/tools/internal/typesinternal/toonew.go @@ -13,20 +13,26 @@ import ( // TooNewStdSymbols computes the set of package-level symbols // exported by pkg that are not available at the specified version. -// The result maps each symbol to its minimum version. // // The pkg is allowed to contain type errors. -func TooNewStdSymbols(pkg *types.Package, version string) map[types.Object]string { - disallowed := make(map[types.Object]string) +func TooNewStdSymbols(pkg *types.Package, version string) map[types.Object]stdlib.Symbol { + disallowed := make(map[types.Object]stdlib.Symbol) + + // Some symbols are accessible before their release but + // only with specific build tags unknown to us here. + // Avoid false positives in such cases. + if pkg.Path() == "testing/synctest" && versions.AtLeast(version, "go1.24") { + // requires go1.24 && goexperiment.synctest || go1.25 + return disallowed + } // Pass 1: package-level symbols. symbols := stdlib.PackageSymbols[pkg.Path()] for _, sym := range symbols { - symver := sym.Version.String() - if versions.Before(version, symver) { + if versions.Before(version, sym.Version.String()) { switch sym.Kind { case stdlib.Func, stdlib.Var, stdlib.Const, stdlib.Type: - disallowed[pkg.Scope().Lookup(sym.Name)] = symver + disallowed[pkg.Scope().Lookup(sym.Name)] = sym } } } @@ -60,28 +66,36 @@ func TooNewStdSymbols(pkg *types.Package, version string) map[types.Object]strin // spuriously cause the analyzer to report a reference to a // too-new symbol even though this expression compiles just // fine (with the fake implementation) using go1.21. + var noSym stdlib.Symbol + depth := make(map[types.Object]int) for _, sym := range symbols { - symVersion := sym.Version.String() - if !versions.Before(version, symVersion) { + if !versions.Before(version, sym.Version.String()) { continue // allowed } var obj types.Object + var indices []int switch sym.Kind { case stdlib.Field: typename, name := sym.SplitField() - if t := pkg.Scope().Lookup(typename); t != nil && disallowed[t] == "" { - obj, _, _ = types.LookupFieldOrMethod(t.Type(), false, pkg, name) + if t := pkg.Scope().Lookup(typename); t != nil && disallowed[t] == noSym { + obj, indices, _ = types.LookupFieldOrMethod(t.Type(), false, pkg, name) } case stdlib.Method: ptr, recvname, name := sym.SplitMethod() - if t := pkg.Scope().Lookup(recvname); t != nil && disallowed[t] == "" { - obj, _, _ = types.LookupFieldOrMethod(t.Type(), ptr, pkg, name) + if t := pkg.Scope().Lookup(recvname); t != nil && disallowed[t] == noSym { + obj, indices, _ = types.LookupFieldOrMethod(t.Type(), ptr, pkg, name) } } if obj != nil { - disallowed[obj] = symVersion + // In the presence of embedding, two or more "pkg.T.name" + // strings may map to the same types.Object. + // Prefer the Object with the shorter index path. + if min, ok := depth[obj]; !ok || len(indices) < min { + depth[obj] = len(indices) + disallowed[obj] = sym + } } } diff --git a/src/cmd/vendor/modules.txt b/src/cmd/vendor/modules.txt index bbe443fd3a6105..6e513d8a5f175e 100644 --- a/src/cmd/vendor/modules.txt +++ b/src/cmd/vendor/modules.txt @@ -73,7 +73,7 @@ golang.org/x/text/internal/tag golang.org/x/text/language golang.org/x/text/transform golang.org/x/text/unicode/norm -# golang.org/x/tools v0.45.1-0.20260710204130-3cdf838841de +# golang.org/x/tools v0.45.1-0.20260714184632-28657d66a087 ## explicit; go 1.25.0 golang.org/x/tools/cmd/bisect golang.org/x/tools/cover diff --git a/src/encoding/json/v2/example_test.go b/src/encoding/json/v2/example_test.go index 2596a10abfde1d..c0a8587425d668 100644 --- a/src/encoding/json/v2/example_test.go +++ b/src/encoding/json/v2/example_test.go @@ -10,6 +10,7 @@ import ( "bytes" "errors" "fmt" + "io" "log" "net/http" "net/netip" @@ -559,3 +560,31 @@ func ExampleWithUnmarshalers_recordOffsets() { // Output: // 3:3: source and destination must both be specified } + +// UnmarshalDecode can be used to unmarshal a stream of whitespace-delimited +// JSON values. +func ExampleUnmarshalDecode_stream() { + const jsonStream = ` + {"Name": "Platypus", "Order": "Monotremata"} + {"Name": "Quoll", "Order": "Dasyuromorphia"} + {"Name": "Gopher", "Order": "Rodentia"} +` + type Animal struct { + Name string + Order string + } + dec := jsontext.NewDecoder(strings.NewReader(jsonStream)) + for { + var a Animal + if err := json.UnmarshalDecode(dec, &a); err == io.EOF { + break + } else if err != nil { + log.Fatal(err) + } + fmt.Printf("%s: %s\n", a.Name, a.Order) + } + // Output: + // Platypus: Monotremata + // Quoll: Dasyuromorphia + // Gopher: Rodentia +} diff --git a/src/encoding/pem/pem.go b/src/encoding/pem/pem.go index 6bf2b41ad0eb7f..0498a45c188d53 100644 --- a/src/encoding/pem/pem.go +++ b/src/encoding/pem/pem.go @@ -307,8 +307,12 @@ func Encode(out io.Writer, b *Block) error { if _, err := b64.Write(b.Bytes); err != nil { return err } - b64.Close() - breaker.Close() + if err := b64.Close(); err != nil { + return err + } + if err := breaker.Close(); err != nil { + return err + } if _, err := out.Write(pemEnd[1:]); err != nil { return err diff --git a/src/internal/goversion/goversion.go b/src/internal/goversion/goversion.go index b33e0d46cc31fe..abba8ccadcc44b 100644 --- a/src/internal/goversion/goversion.go +++ b/src/internal/goversion/goversion.go @@ -9,4 +9,4 @@ package goversion // // It should be updated at the start of each development cycle to be // the version of the next Go 1.x release. See go.dev/issue/40705. -const Version = 27 +const Version = 28 diff --git a/src/net/http/client_test.go b/src/net/http/client_test.go index 895b4024840df6..edbeae0764c784 100644 --- a/src/net/http/client_test.go +++ b/src/net/http/client_test.go @@ -2240,7 +2240,7 @@ func testClientCallsCloseOnlyOnce(t *testing.T, mode testMode) { // Issue occurred non-deterministically: needed to occur after a successful // write (into TCP buffer) but before end of body. for i := 0; i < 50 && !t.Failed(); i++ { - body := &issue40382Body{t: t, n: 300000} + body := &issue40382Body{t: t, n: 256 << 10} // maxPostHandlerReadBytes large req, err := NewRequest(MethodPost, cst.ts.URL, body) if err != nil { t.Fatal(err) diff --git a/src/net/http/fs.go b/src/net/http/fs.go index 821b9b1266d21f..240a8dc675ddc5 100644 --- a/src/net/http/fs.go +++ b/src/net/http/fs.go @@ -784,6 +784,12 @@ func toHTTPError(err error) (msg string, httpStatus int) { // localRedirect gives a Moved Permanently response. // It does not convert relative paths to absolute paths like Redirect does. func localRedirect(w ResponseWriter, r *Request, newPath string) { + // There is no reliable way for us to redirect correctly when the path has + // escaped slashes, since StripPrefix might be in use. Just return 404. + if p := r.URL.EscapedPath(); strings.Contains(p, "%2f") || strings.Contains(p, "%2F") { + NotFound(w, r) + return + } if q := r.URL.RawQuery; q != "" { newPath += "?" + q } diff --git a/src/net/http/fs_test.go b/src/net/http/fs_test.go index d7859b9e9c9f87..6edaab03af7235 100644 --- a/src/net/http/fs_test.go +++ b/src/net/http/fs_test.go @@ -269,10 +269,19 @@ func TestServeContentWithEmptyContentIgnoreRanges(t *testing.T) { var fsRedirectTestData = []struct { original, redirect string + status int }{ - {"/test/index.html", "/test/"}, - {"/test/testdata", "/test/testdata/"}, - {"/test/testdata/file/", "/test/testdata/file"}, + {"/test/index.html", "/test/", 200}, + {"/test/testdata", "/test/testdata/", 200}, + {"/test/testdata/file/", "/test/testdata/file", 200}, + // Redirect attempts for path with escaped slashes should result in 404. + // However, escaped paths are okay if they do not trigger a redirect. + // See https://go.dev/issue/80289. + {"/test%2ftestdata", "/test/testdata", 404}, + {"/test/testdata%2ffile/", "/test/testdata/file/", 404}, + {"/test/testdata%2Findex.html", "/test/testdata/index.html", 404}, + {"/test/testdata%2ffile", "/test/testdata/file", 200}, + {"/test/testdata%2F", "/test/testdata/", 200}, } func TestFSRedirect(t *testing.T) { run(t, testFSRedirect) } @@ -288,6 +297,9 @@ func testFSRedirect(t *testing.T, mode testMode) { if g, e := res.Request.URL.Path, data.redirect; g != e { t.Errorf("redirect from %s: got %s, want %s", data.original, g, e) } + if res.StatusCode != data.status { + t.Errorf("redirect from %s: got status %d, want %d", data.original, res.StatusCode, data.status) + } } } diff --git a/src/net/http/http1_server_test.go b/src/net/http/http1_server_test.go new file mode 100644 index 00000000000000..17c31a877c08f0 --- /dev/null +++ b/src/net/http/http1_server_test.go @@ -0,0 +1,114 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package http_test + +import ( + "bufio" + "errors" + "internal/nettest" + "internal/synctest" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// An http1ServerTest tests an HTTP/1 server using a fake network. +// It must be used in a synctest bubble. +type http1ServerTest struct { + t *testing.T + ts *httptest.Server +} + +func newHTTP1ServerTest(t *testing.T, h http.HandlerFunc) *http1ServerTest { + if h == nil { + h = func(w http.ResponseWriter, req *http.Request) {} + } + st := &http1ServerTest{ + t: t, + ts: httptest.NewTestServer(t, h), + } + return st +} + +// client returns a Client that sends requests to the server. +func (st *http1ServerTest) client() *http.Client { + return st.ts.Client() +} + +// transport returns a Transport that sends requests to the server. +func (st *http1ServerTest) transport() *http.Transport { + return st.ts.Client().Transport.(*http.Transport) +} + +// dial returns a connection to the server. +func (st *http1ServerTest) dial() *http1TestConn { + t := st.t + t.Helper() + nc, err := st.transport().DialContext(st.t.Context(), "tcp", "example.tld") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + nc.Close() + }) + conn := nc.(*nettest.Conn) + conn.SetReadError(errWouldBlock) // effectively make reads non-blocking + return &http1TestConn{ + t: st.t, + conn: conn, + bufr: bufio.NewReader(conn), + } +} + +var errWouldBlock = errors.New("would block") + +type http1TestConn struct { + t *testing.T + conn *nettest.Conn + bufr *bufio.Reader +} + +// writeMessage writes a number of CRLF-terminated lines to the connection. +func (tc *http1TestConn) writeMessage(lines ...string) { + t := tc.t + t.Helper() + if _, err := tc.conn.Write([]byte(strings.Join(lines, "\r\n") + "\r\n")); err != nil { + t.Fatalf("conn write: %v", err) + } +} + +// readResponse reads a response from the connection (not including the response body). +func (tc *http1TestConn) readResponse() *http.Response { + t := tc.t + t.Helper() + synctest.Wait() + resp, err := http.ReadResponse(tc.bufr, nil) + if err != nil { + t.Fatalf("ReadResponse: %v", err) + } + return resp +} + +// wantIdle asserts that the connection is not closed and has no pending data to read. +func (tc *http1TestConn) wantIdle() { + t := tc.t + t.Helper() + synctest.Wait() + if got, err := tc.bufr.Peek(32); len(got) != 0 || !errors.Is(err, errWouldBlock) { + t.Fatalf("read from conn: %q, %v; expect conn to be idle", got, err) + } +} + +// wantClosed asserts that the connection is read-closed and has no pending data to read. +func (tc *http1TestConn) wantClosed() { + t := tc.t + t.Helper() + synctest.Wait() + if got, err := tc.bufr.Peek(32); len(got) != 0 || err != io.EOF { + t.Fatalf("read from conn: %q; expect conn to be closed", got) + } +} diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go index c681e01f423e3a..19aa63f8cc5beb 100644 --- a/src/net/http/serve_test.go +++ b/src/net/http/serve_test.go @@ -7938,3 +7938,184 @@ func TestServerHTTP2Disabled(t *testing.T) { srv.Shutdown(t.Context()) }) } + +func TestServerConnectionReuse(t *testing.T) { + for _, test := range []struct { + name string + message []string + handler HandlerFunc + continueBodySize int + want100Continue bool + wantResponse int + wantReused bool + skip string + }{{ + name: "small body", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Content-Length: 1", + "", + "x", + }, + wantResponse: 200, + wantReused: true, + }, { + name: "large body", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Content-Length: 300000", // more than maxPostHandlerReadBytes + "", + // body is never sent + }, + wantResponse: 200, + wantReused: false, + }, { + name: "small body full duplex", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Content-Length: 1", + "", + "x", + }, + handler: func(w ResponseWriter, req *Request) { + // Enable full duplex to avoid trying to read the request before + // writing the response. + NewResponseController(w).EnableFullDuplex() + }, + wantResponse: 200, + wantReused: true, + }, { + name: "large body full duplex", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Content-Length: 300000", // more than maxPostHandlerReadBytes + "", + // body is never sent + }, + handler: func(w ResponseWriter, req *Request) { + // Enable full duplex to avoid trying to read the request before + // writing the response. + NewResponseController(w).EnableFullDuplex() + }, + wantResponse: 200, + wantReused: false, + }, { + // Send a request with a 1-byte body, which the server handler never reads. + // We should either send a 100-Continue and read the body + // or we should close the connection. + // + // Right now, the server hangs trying to read the request body + // the client isn't sending. + skip: "https://go.dev/issue/75933", + + name: "100-continue unconsumed small body", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Expect: 100-continue", + "Content-Length: 1", + "", + // body is never sent + }, + want100Continue: false, + wantResponse: 200, + wantReused: true, + }, { + name: "100-continue unconsumed large body", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Expect: 100-continue", + "Content-Length: 300000", // more than maxPostHandlerReadBytes + "", + // body is never sent + }, + want100Continue: false, + wantResponse: 200, + wantReused: false, + }, { + name: "100-continue consumed small body", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Expect: 100-continue", + "Content-Length: 1", + "", + }, + handler: func(w ResponseWriter, req *Request) { + io.Copy(io.Discard, req.Body) + }, + want100Continue: true, + continueBodySize: 1, + wantResponse: 200, + wantReused: true, + }, { + name: "small wrapped body", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Content-Length: 1", + "", + "x", + }, + handler: func(w ResponseWriter, req *Request) { + // Enable full duplex to avoid trying to read the request before + // writing the response. + NewResponseController(w).EnableFullDuplex() + + // Middleware wraps the Request.Body in some other type. + req.Body = struct{ io.ReadCloser }{req.Body} + }, + wantResponse: 200, + wantReused: true, + }, { + name: "large wrapped body", + message: []string{ + "POST / HTTP/1.1", + "Host: example.tld", + "Content-Length: 300000", // more than maxPostHandlerReadBytes + "", + }, + handler: func(w ResponseWriter, req *Request) { + // Enable full duplex to avoid trying to read the request before + // writing the response. + NewResponseController(w).EnableFullDuplex() + + // Middleware wraps the Request.Body in some other type. + req.Body = struct{ io.ReadCloser }{req.Body} + }, + wantResponse: 200, + wantReused: false, + }} { + t.Run(test.name, func(t *testing.T) { + if test.skip != "" { + t.Skip(test.skip) + } + synctest.Test(t, func(t *testing.T) { + st := newHTTP1ServerTest(t, test.handler) + conn := st.dial() + conn.writeMessage(test.message...) + resp := conn.readResponse() + if got, want := resp.StatusCode == 100, test.want100Continue; got != want { + t.Fatalf("100-Continue response: %v, want %v", got, want) + } + if resp.StatusCode == 100 { + conn.conn.Write(bytes.Repeat([]byte("x"), test.continueBodySize)) + resp = conn.readResponse() + } + if got, want := resp.StatusCode, test.wantResponse; got != want { + t.Fatalf("got response %v, want %v", got, want) + } + if test.wantReused { + conn.wantIdle() + } else { + conn.wantClosed() + } + }) + }) + } +} diff --git a/src/net/http/server.go b/src/net/http/server.go index ae2142705d4640..cdf5c43c2ab040 100644 --- a/src/net/http/server.go +++ b/src/net/http/server.go @@ -426,12 +426,13 @@ func (cw *chunkWriter) close() { // A response represents the server side of an HTTP response. type response struct { conn *conn - req *Request // request for this response - reqBody io.ReadCloser + req *Request // request for this response + reqBody *body // nil when NoBody cancelCtx context.CancelFunc // when ServeHTTP exits wroteHeader bool // a non-1xx header has been (logically) written wants10KeepAlive bool // HTTP/1.0 w/ Connection "keep-alive" wantsClose bool // HTTP request has Connection "close" + ecReader *expectContinueReader // canWriteContinue is an atomic boolean that says whether or // not a 100 Continue header can be written to the @@ -579,8 +580,7 @@ func (w *response) requestTooLarge() { // the request body and flags the connection to be closed after the reply, as // the client will never send the body. func (w *response) disableWriteContinue(skipDrain bool) { - ecr, ok := w.reqBody.(*expectContinueReader) - if !ok { + if w.ecReader == nil { return } w.writeContinueMu.Lock() @@ -591,7 +591,7 @@ func (w *response) disableWriteContinue(skipDrain bool) { // "Connection: close" header in the response. w.closeAfterReply = true // Ensure that the body will not be drained in Close. - ecr.closed.Store(true) + w.ecReader.closed.Store(true) } } w.writeContinueMu.Unlock() @@ -989,7 +989,6 @@ type expectContinueReader struct { resp *response readCloser io.ReadCloser closed atomic.Bool - sawEOF atomic.Bool } func (ecr *expectContinueReader) Read(p []byte) (n int, err error) { @@ -1006,11 +1005,7 @@ func (ecr *expectContinueReader) Read(p []byte) (n int, err error) { } w.writeContinueMu.Unlock() } - n, err = ecr.readCloser.Read(p) - if err == io.EOF { - ecr.sawEOF.Store(true) - } - return + return ecr.readCloser.Read(p) } func (ecr *expectContinueReader) Close() error { @@ -1095,8 +1090,14 @@ func (c *conn) readRequest(ctx context.Context) (w *response, err error) { req.ctx = ctx req.RemoteAddr = c.remoteAddr req.TLS = c.tlsState - if body, ok := req.Body.(*body); ok { - body.doEarlyClose = true + var reqBody *body + switch b := req.Body.(type) { + case noBody: + case *body: + reqBody = b + reqBody.doEarlyClose = true + default: + panic(fmt.Errorf("http: unexpected request body type %T", req.Body)) } c.rwc.SetReadDeadline(wholeReqDeadline) @@ -1105,7 +1106,7 @@ func (c *conn) readRequest(ctx context.Context) (w *response, err error) { conn: c, cancelCtx: cancelCtx, req: req, - reqBody: req.Body, + reqBody: reqBody, handlerHeader: make(Header), contentLength: -1, @@ -1425,7 +1426,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { // than to read the body. For now, assume that if we're sending // headers, the handler is done reading the body and we should // drop the connection if we haven't seen EOF. - if ecr, ok := w.req.Body.(*expectContinueReader); ok && !ecr.sawEOF.Load() { + if w.ecReader != nil && w.reqBody.bodyRemains() { w.closeAfterReply = true } @@ -1443,53 +1444,28 @@ func (cw *chunkWriter) writeHeader(p []byte) { // before reading a response may deadlock in this case. // This behavior has been present since CL 5268043 (2011), however, // so it doesn't seem to be causing problems. - if w.req.ContentLength != 0 && !w.closeAfterReply && !w.fullDuplex { + if w.req.ContentLength != 0 && w.reqBody != nil && !w.closeAfterReply && !w.fullDuplex { var discard, tooBig bool - - switch bdy := w.req.Body.(type) { - case *expectContinueReader: - // We only get here if we have already fully consumed the request body - // (see above). - case *body: - bdy.mu.Lock() - switch { - case bdy.closed: - if !bdy.sawEOF { - // Body was closed in handler with non-EOF error. - w.closeAfterReply = true - } - case bdy.unreadDataSizeLocked() >= maxPostHandlerReadBytes: - tooBig = true - default: - discard = true + w.reqBody.mu.Lock() + switch { + case w.reqBody.closed: + if !w.reqBody.sawEOF { + // Body was closed in handler with non-EOF error. + w.closeAfterReply = true } - bdy.mu.Unlock() + case w.reqBody.unreadDataSizeLocked() >= maxPostHandlerReadBytes: + tooBig = true default: discard = true } + w.reqBody.mu.Unlock() if discard { - _, err := io.CopyN(io.Discard, w.reqBody, maxPostHandlerReadBytes+1) - switch err { - case nil: - // There must be even more data left over. - tooBig = true - case ErrBodyReadAfterClose: - // Body was already consumed and closed. - case io.EOF: - // The remaining body was just consumed, close it. - err = w.reqBody.Close() - if err != nil { - w.closeAfterReply = true - } - default: - // Some other kind of error occurred, like a read timeout, or - // corrupt chunked encoding. In any case, whatever remains - // on the wire must not be parsed as another HTTP request. + w.reqBody.Close() + if w.reqBody.didEarlyClose() { w.closeAfterReply = true } } - if tooBig { w.requestTooLarge() delHeader("Connection") @@ -1728,6 +1704,7 @@ func (w *response) finishRequest() { w.conn.bufw.Flush() w.conn.r.abortPendingRead() + w.reqBody.registerOnHitEOF(nil) // prevent new background read from starting if w.canWriteContinue.Load() { w.disableWriteContinue(true) @@ -1735,6 +1712,8 @@ func (w *response) finishRequest() { // Close the body (regardless of w.closeAfterReply) so we can // re-use its bufio.Reader later safely. + // + // In full-duplex mode, this may also drain the remaining request body. w.reqBody.Close() if w.req.MultipartForm != nil { @@ -1771,8 +1750,7 @@ func (w *response) shouldReuseConnection() bool { } func (w *response) closedRequestBodyEarly() bool { - body, ok := w.req.Body.(*body) - return ok && body.didEarlyClose() + return w.reqBody != nil && w.reqBody.didEarlyClose() } func (w *response) Flush() { @@ -2128,9 +2106,9 @@ func (c *conn) serve(ctx context.Context) { if req.expectsContinue() { if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { // Wrap the Body reader with one that replies on the connection - req.Body = &expectContinueReader{readCloser: req.Body, resp: w} + w.ecReader = &expectContinueReader{readCloser: req.Body, resp: w} w.canWriteContinue.Store(true) - w.reqBody = req.Body + req.Body = w.ecReader } } else if req.Header.get("Expect") != "" { w.sendExpectationFailed() @@ -2139,8 +2117,11 @@ func (c *conn) serve(ctx context.Context) { c.curReq.Store(w) - if requestBodyRemains(req.Body) { - registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead) + // Start background read, which detects when a client has closed its connection + // while a request handler is still running. When the request has a body, we + // start the background read only after the entire body has been consumed. + if w.reqBody.bodyRemains() { + w.reqBody.registerOnHitEOF(w.conn.r.startBackgroundRead) } else { w.conn.r.startBackgroundRead() } @@ -2344,33 +2325,6 @@ func (w *response) closeNotify() { } } -func registerOnHitEOF(rc io.ReadCloser, fn func()) { - switch v := rc.(type) { - case *expectContinueReader: - registerOnHitEOF(v.readCloser, fn) - case *body: - v.registerOnHitEOF(fn) - default: - panic("unexpected type " + fmt.Sprintf("%T", rc)) - } -} - -// requestBodyRemains reports whether future calls to Read -// on rc might yield more data. -func requestBodyRemains(rc io.ReadCloser) bool { - if rc == NoBody { - return false - } - switch v := rc.(type) { - case *expectContinueReader: - return requestBodyRemains(v.readCloser) - case *body: - return v.bodyRemains() - default: - panic("unexpected type " + fmt.Sprintf("%T", rc)) - } -} - // The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a diff --git a/src/net/http/transfer.go b/src/net/http/transfer.go index 1a3afc6f5fbd5c..451a7d2263f8ad 100644 --- a/src/net/http/transfer.go +++ b/src/net/http/transfer.go @@ -838,6 +838,9 @@ type body struct { var ErrBodyReadAfterClose = errors.New("http: invalid Read on closed Body") func (b *body) Read(p []byte) (n int, err error) { + if b == nil { + return 0, io.EOF + } b.mu.Lock() defer b.mu.Unlock() if b.closed { @@ -978,6 +981,9 @@ func (b *body) unreadDataSizeLocked() int64 { } func (b *body) Close() error { + if b == nil { + return nil + } b.mu.Lock() defer b.mu.Unlock() if b.closed { @@ -1001,12 +1007,11 @@ func (b *body) Close() error { var n int64 // Consume the body, or, which will also lead to us reading // the trailer headers after the body, if present. - n, err = io.CopyN(io.Discard, bodyLocked{b}, maxPostHandlerReadBytes) - if err == io.EOF { - err = nil - } - if n == maxPostHandlerReadBytes { - b.earlyClose = true + n, err = io.CopyN(io.Discard, bodyLocked{b}, maxPostHandlerReadBytes+1) + b.earlyClose = true + if err == io.EOF && n <= maxPostHandlerReadBytes { + b.earlyClose = false + b.sawEOF = true } } default: @@ -1027,12 +1032,18 @@ func (b *body) didEarlyClose() bool { // bodyRemains reports whether future Read calls might // yield data. func (b *body) bodyRemains() bool { + if b == nil { + return false + } b.mu.Lock() defer b.mu.Unlock() return !b.sawEOF } func (b *body) registerOnHitEOF(fn func()) { + if b == nil { + return + } b.mu.Lock() defer b.mu.Unlock() b.onHitEOF = fn