Skip to content

Commit

Permalink
net/http: allow sending 1xx responses
Browse files Browse the repository at this point in the history
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 golang#26089.
Fixes golang#36734.
Updates golang#26088.
  • Loading branch information
dunglas committed May 17, 2022
1 parent 41b9d8c commit 127f6c6
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 5 deletions.
33 changes: 33 additions & 0 deletions src/net/http/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6725,3 +6725,36 @@ func testMaxBytesHandler(t *testing.T, maxSize, requestSize int64) {
t.Errorf("expected echo of size %d; got %d", handlerN, buf.Len())
}
}

func TestEarlyHints(t *testing.T) {
ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
h := w.Header()
h.Add("Link", "</style.css>; rel=preload; as=style")
h.Add("Link", "</script.js>; rel=preload; as=script")
w.WriteHeader(StatusEarlyHints)

h.Add("Link", "</foo.js>; rel=preload; as=script")
w.WriteHeader(StatusEarlyHints)

w.Write([]byte("stuff"))
}))

got := ht.rawResponse("GET / HTTP/1.1\nHost: golang.org")
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
if !strings.Contains(got, expected) {
t.Errorf("unexpected response; got %q; should start by %q", got, expected)
}
}

func TestProcessing(t *testing.T) {
ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
w.WriteHeader(StatusProcessing)
w.Write([]byte("stuff"))
}))

got := ht.rawResponse("GET / HTTP/1.1\nHost: golang.org")
expected := "HTTP/1.1 102 Processing\r\n\r\nHTTP/1.1 200 OK\r\nDate: " // dynamic content expected
if !strings.Contains(got, expected) {
t.Errorf("unexpected response; got %q; should start by %q", got, expected)
}
}
30 changes: 25 additions & 5 deletions src/net/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,20 @@ type ResponseWriter interface {
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
// send error codes or informational responses.
//
// The provided code must be a valid HTTP 1xx-5xx status code.
// Only one header may be written. Go does not currently
// support sending user-defined 1xx informational headers,
// with the exception of 100-continue response header that the
// Server sends automatically when the Request.Body is read.
// Only one header with a status code not in the 1xx range may
// be written.
//
// Informational headers (status in the 1xx range) can be sent multiple
// times before sending the final header.
//
// If the passed status code is 103, the current content of the header
// map will be sent as early hints for the client.
//
// The server automatically sends the 100-continue response header
// when the Request.Body is read.
WriteHeader(statusCode int)
}

Expand Down Expand Up @@ -1144,6 +1151,19 @@ func (w *response) WriteHeader(code int) {
return
}
checkWriteHeaderCode(code)

// Handle provisional headers, except 100 (continue) which is handled automatically
if code > 100 && code < 200 {
writeStatusLine(w.conn.bufw, w.req.ProtoAtLeast(1, 1), code, w.statusBuf[:])
if code == 103 {
// Per RFC 8297 we must not clear the current header map
w.handlerHeader.Write(w.conn.bufw)
}
w.conn.bufw.Write(crlf)

return
}

w.wroteHeader = true
w.status = code

Expand Down

0 comments on commit 127f6c6

Please sign in to comment.