Skip to content

Commit 770e0e5

Browse files
dunglasneild
authored andcommitted
net/http: allow sending 1xx responses
Currently, it's not possible to send informational responses such as 103 Early Hints or 102 Processing. This patch allows calling WriteHeader() multiple times in order to send informational responses before the final one. If the status code is in the 1xx range, the current content of the header map is also sent. Its content is not removed after the call to WriteHeader() because the headers must also be included in the final response. The Chrome and Fastly teams are starting a large-scale experiment to measure the real-life impact of the 103 status code. Using Early Hints is proposed as a (partial) alternative to Server Push, which are going to be removed from Chrome: https://groups.google.com/a/chromium.org/g/blink-dev/c/K3rYLvmQUBY/m/21anpFhxAQAJ Being able to send this status code from servers implemented using Go would help to see if implementing it in browsers is worth it. Fixes #26089 Fixes #36734 Updates #26088 Change-Id: Ib7023c1892c35e8915d4305dd7f6373dbd00a19d GitHub-Last-Rev: 06d749d GitHub-Pull-Request: #42597 Reviewed-on: https://go-review.googlesource.com/c/go/+/269997 Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent cf7ec0f commit 770e0e5

File tree

4 files changed

+163
-11
lines changed

4 files changed

+163
-11
lines changed

src/net/http/clientserver_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package http_test
99
import (
1010
"bytes"
1111
"compress/gzip"
12+
"context"
1213
"crypto/rand"
1314
"crypto/sha1"
1415
"crypto/tls"
@@ -19,7 +20,9 @@ import (
1920
"net"
2021
. "net/http"
2122
"net/http/httptest"
23+
"net/http/httptrace"
2224
"net/http/httputil"
25+
"net/textproto"
2326
"net/url"
2427
"os"
2528
"reflect"
@@ -1616,3 +1619,95 @@ func testIdentityTransferEncoding(t *testing.T, h2 bool) {
16161619
t.Errorf("got response body = %q; want %q", got, want)
16171620
}
16181621
}
1622+
1623+
func TestEarlyHintsRequest_h1(t *testing.T) { testEarlyHintsRequest(t, h1Mode) }
1624+
func TestEarlyHintsRequest_h2(t *testing.T) { testEarlyHintsRequest(t, h2Mode) }
1625+
func testEarlyHintsRequest(t *testing.T, h2 bool) {
1626+
defer afterTest(t)
1627+
if h2 {
1628+
t.Skip("Waiting for H2 support to be merged: https://go-review.googlesource.com/c/net/+/406494")
1629+
}
1630+
1631+
var wg sync.WaitGroup
1632+
wg.Add(1)
1633+
cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
1634+
h := w.Header()
1635+
1636+
h.Add("Content-Length", "123") // must be ignored
1637+
h.Add("Link", "</style.css>; rel=preload; as=style")
1638+
h.Add("Link", "</script.js>; rel=preload; as=script")
1639+
w.WriteHeader(StatusEarlyHints)
1640+
1641+
wg.Wait()
1642+
1643+
h.Add("Link", "</foo.js>; rel=preload; as=script")
1644+
w.WriteHeader(StatusEarlyHints)
1645+
1646+
w.Write([]byte("Hello"))
1647+
}))
1648+
defer cst.close()
1649+
1650+
checkLinkHeaders := func(t *testing.T, expected, got []string) {
1651+
t.Helper()
1652+
1653+
if len(expected) != len(got) {
1654+
t.Errorf("got %d expected %d", len(got), len(expected))
1655+
}
1656+
1657+
for i := range expected {
1658+
if expected[i] != got[i] {
1659+
t.Errorf("got %q expected %q", got[i], expected[i])
1660+
}
1661+
}
1662+
}
1663+
1664+
checkExcludedHeaders := func(t *testing.T, header textproto.MIMEHeader) {
1665+
t.Helper()
1666+
1667+
for _, h := range []string{"Content-Length", "Transfer-Encoding"} {
1668+
if v, ok := header[h]; ok {
1669+
t.Errorf("%s is %q; must not be sent", h, v)
1670+
}
1671+
}
1672+
}
1673+
1674+
var respCounter uint8
1675+
trace := &httptrace.ClientTrace{
1676+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
1677+
switch respCounter {
1678+
case 0:
1679+
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"}, header["Link"])
1680+
checkExcludedHeaders(t, header)
1681+
1682+
wg.Done()
1683+
case 1:
1684+
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, header["Link"])
1685+
checkExcludedHeaders(t, header)
1686+
1687+
default:
1688+
t.Error("Unexpected 1xx response")
1689+
}
1690+
1691+
respCounter++
1692+
1693+
return nil
1694+
},
1695+
}
1696+
req, _ := NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), "GET", cst.ts.URL, nil)
1697+
1698+
res, err := cst.c.Do(req)
1699+
if err != nil {
1700+
t.Fatal(err)
1701+
}
1702+
defer res.Body.Close()
1703+
1704+
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, res.Header["Link"])
1705+
if cl := res.Header.Get("Content-Length"); cl != "123" {
1706+
t.Errorf("Content-Length is %q; want 123", cl)
1707+
}
1708+
1709+
body, _ := io.ReadAll(res.Body)
1710+
if string(body) != "Hello" {
1711+
t.Errorf("Read body %q; want Hello", body)
1712+
}
1713+
}

src/net/http/serve_test.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3873,7 +3873,7 @@ func testServerReaderFromOrder(t *testing.T, h2 bool) {
38733873

38743874
// Issue 6157, Issue 6685
38753875
func TestCodesPreventingContentTypeAndBody(t *testing.T) {
3876-
for _, code := range []int{StatusNotModified, StatusNoContent, StatusContinue} {
3876+
for _, code := range []int{StatusNotModified, StatusNoContent} {
38773877
ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
38783878
if r.URL.Path == "/header" {
38793879
w.Header().Set("Content-Length", "123")
@@ -6725,3 +6725,35 @@ func testMaxBytesHandler(t *testing.T, maxSize, requestSize int64) {
67256725
t.Errorf("expected echo of size %d; got %d", handlerN, buf.Len())
67266726
}
67276727
}
6728+
6729+
func TestEarlyHints(t *testing.T) {
6730+
ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
6731+
h := w.Header()
6732+
h.Add("Link", "</style.css>; rel=preload; as=style")
6733+
h.Add("Link", "</script.js>; rel=preload; as=script")
6734+
w.WriteHeader(StatusEarlyHints)
6735+
6736+
h.Add("Link", "</foo.js>; rel=preload; as=script")
6737+
w.WriteHeader(StatusEarlyHints)
6738+
6739+
w.Write([]byte("stuff"))
6740+
}))
6741+
6742+
got := ht.rawResponse("GET / HTTP/1.1\nHost: golang.org")
6743+
expected := "HTTP/1.1 103 Early Hints\r\nLink: </style.css>; rel=preload; as=style\r\nLink: </script.js>; rel=preload; as=script\r\n\r\nHTTP/1.1 103 Early Hints\r\nLink: </style.css>; rel=preload; as=style\r\nLink: </script.js>; rel=preload; as=script\r\nLink: </foo.js>; rel=preload; as=script\r\n\r\nHTTP/1.1 200 OK\r\nLink: </style.css>; rel=preload; as=style\r\nLink: </script.js>; rel=preload; as=script\r\nLink: </foo.js>; rel=preload; as=script\r\nDate: " // dynamic content expected
6744+
if !strings.Contains(got, expected) {
6745+
t.Errorf("unexpected response; got %q; should start by %q", got, expected)
6746+
}
6747+
}
6748+
func TestProcessing(t *testing.T) {
6749+
ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
6750+
w.WriteHeader(StatusProcessing)
6751+
w.Write([]byte("stuff"))
6752+
}))
6753+
6754+
got := ht.rawResponse("GET / HTTP/1.1\nHost: golang.org")
6755+
expected := "HTTP/1.1 102 Processing\r\n\r\nHTTP/1.1 200 OK\r\nDate: " // dynamic content expected
6756+
if !strings.Contains(got, expected) {
6757+
t.Errorf("unexpected response; got %q; should start by %q", got, expected)
6758+
}
6759+
}

src/net/http/server.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ type ResponseWriter interface {
9898
// Handlers can set HTTP trailers.
9999
//
100100
// Changing the header map after a call to WriteHeader (or
101-
// Write) has no effect unless the modified headers are
102-
// trailers.
101+
// Write) has no effect unless the HTTP status code was of the
102+
// 1xx class or the modified headers are trailers.
103103
//
104104
// There are two ways to set Trailers. The preferred way is to
105105
// predeclare in the headers which trailers you will later
@@ -144,13 +144,18 @@ type ResponseWriter interface {
144144
// If WriteHeader is not called explicitly, the first call to Write
145145
// will trigger an implicit WriteHeader(http.StatusOK).
146146
// Thus explicit calls to WriteHeader are mainly used to
147-
// send error codes.
147+
// send error codes or 1xx informational responses.
148148
//
149149
// The provided code must be a valid HTTP 1xx-5xx status code.
150-
// Only one header may be written. Go does not currently
151-
// support sending user-defined 1xx informational headers,
152-
// with the exception of 100-continue response header that the
153-
// Server sends automatically when the Request.Body is read.
150+
// Any number of 1xx headers may be written, followed by at most
151+
// one 2xx-5xx header. 1xx headers are sent immediately, but 2xx-5xx
152+
// headers may be buffered. Use the Flusher interface to send
153+
// buffered data. The header map is cleared when 2xx-5xx headers are
154+
// sent, but not with 1xx headers.
155+
//
156+
// The server will automatically send a 100 (Continue) header
157+
// on the first read from the request body if the request has
158+
// an "Expect: 100-continue" header.
154159
WriteHeader(statusCode int)
155160
}
156161

@@ -420,7 +425,7 @@ type response struct {
420425
req *Request // request for this response
421426
reqBody io.ReadCloser
422427
cancelCtx context.CancelFunc // when ServeHTTP exits
423-
wroteHeader bool // reply header has been (logically) written
428+
wroteHeader bool // a non-1xx header has been (logically) written
424429
wroteContinue bool // 100 Continue response was written
425430
wants10KeepAlive bool // HTTP/1.0 w/ Connection "keep-alive"
426431
wantsClose bool // HTTP request has Connection "close"
@@ -1100,8 +1105,7 @@ func checkWriteHeaderCode(code int) {
11001105
// Issue 22880: require valid WriteHeader status codes.
11011106
// For now we only enforce that it's three digits.
11021107
// In the future we might block things over 599 (600 and above aren't defined
1103-
// at https://httpwg.org/specs/rfc7231.html#status.codes)
1104-
// and we might block under 200 (once we have more mature 1xx support).
1108+
// at https://httpwg.org/specs/rfc7231.html#status.codes).
11051109
// But for now any three digits.
11061110
//
11071111
// We used to send "HTTP/1.1 000 0" on the wire in responses but there's
@@ -1144,6 +1148,26 @@ func (w *response) WriteHeader(code int) {
11441148
return
11451149
}
11461150
checkWriteHeaderCode(code)
1151+
1152+
// Handle informational headers
1153+
if code >= 100 && code <= 199 {
1154+
// Prevent a potential race with an automatically-sent 100 Continue triggered by Request.Body.Read()
1155+
if code == 100 && w.canWriteContinue.isSet() {
1156+
w.writeContinueMu.Lock()
1157+
w.canWriteContinue.setFalse()
1158+
w.writeContinueMu.Unlock()
1159+
}
1160+
1161+
writeStatusLine(w.conn.bufw, w.req.ProtoAtLeast(1, 1), code, w.statusBuf[:])
1162+
1163+
// Per RFC 8297 we must not clear the current header map
1164+
w.handlerHeader.WriteSubset(w.conn.bufw, excludedHeadersNoBody)
1165+
w.conn.bufw.Write(crlf)
1166+
w.conn.bufw.Flush()
1167+
1168+
return
1169+
}
1170+
11471171
w.wroteHeader = true
11481172
w.status = code
11491173

src/net/http/transfer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ func bodyAllowedForStatus(status int) bool {
468468
var (
469469
suppressedHeaders304 = []string{"Content-Type", "Content-Length", "Transfer-Encoding"}
470470
suppressedHeadersNoBody = []string{"Content-Length", "Transfer-Encoding"}
471+
excludedHeadersNoBody = map[string]bool{"Content-Length": true, "Transfer-Encoding": true}
471472
)
472473

473474
func suppressedHeaders(status int) []string {

0 commit comments

Comments
 (0)