Skip to content

Commit

Permalink
x/net/http2: 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.

In conformance with RFC 8297, if the status code is 103 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.
Updates #42597.
  • Loading branch information
dunglas committed Feb 10, 2021
1 parent 5f4716e commit 2f6bd1b
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 8 deletions.
38 changes: 30 additions & 8 deletions http2/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2611,8 +2611,7 @@ func checkWriteHeaderCode(code int) {
// Issue 22880: require valid WriteHeader status codes.
// For now we only enforce that it's three digits.
// In the future we might block things over 599 (600 and above aren't defined
// at http://httpwg.org/specs/rfc7231.html#status.codes)
// and we might block under 200 (once we have more mature 1xx support).
// at http://httpwg.org/specs/rfc7231.html#status.codes).
// But for now any three digits.
//
// We used to send "HTTP/1.1 000 0" on the wire in responses but there's
Expand All @@ -2633,13 +2632,36 @@ func (w *responseWriter) WriteHeader(code int) {
}

func (rws *responseWriterState) writeHeader(code int) {
if !rws.wroteHeader {
checkWriteHeaderCode(code)
rws.wroteHeader = true
rws.status = code
if len(rws.handlerHeader) > 0 {
rws.snapHeader = cloneHeader(rws.handlerHeader)
if rws.wroteHeader {
return
}

checkWriteHeaderCode(code)

// Handle informational headers, except 100 (Continue) which is handled automatically
if code > 100 && code < 200 {
var h http.Header
if code == 103 {
// Per RFC 8297 we must not clear the current header map
h = rws.handlerHeader
}

if rws.conn.writeHeaders(rws.stream, &writeResHeaders{
streamID: rws.stream.id,
httpResCode: code,
h: h,
endStream: rws.handlerDone && !rws.hasTrailers(),
}) != nil {
rws.dirty = true
}

return
}

rws.wroteHeader = true
rws.status = code
if len(rws.handlerHeader) > 0 {
rws.snapHeader = cloneHeader(rws.handlerHeader)
}
}

Expand Down
89 changes: 89 additions & 0 deletions http2/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4267,3 +4267,92 @@ func TestServerWindowUpdateOnBodyClose(t *testing.T) {
t.Error(err)
}
}

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

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

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

return nil
}, func(st *serverTester) {
getSlash(st)
hf := st.wantHeaders()
goth := st.decodeHeader(hf.HeaderBlockFragment())
wanth := [][2]string{
{":status", "103"},
{"link", "</style.css>; rel=preload; as=style"},
{"link", "</script.js>; rel=preload; as=script"},
}

if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Got = %q; want %q", goth, wanth)
}

hf = st.wantHeaders()
goth = st.decodeHeader(hf.HeaderBlockFragment())
wanth = [][2]string{
{":status", "103"},
{"link", "</style.css>; rel=preload; as=style"},
{"link", "</script.js>; rel=preload; as=script"},
{"link", "</foo.js>; rel=preload; as=script"},
}

if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Got = %q; want %q", goth, wanth)
}

hf = st.wantHeaders()
goth = st.decodeHeader(hf.HeaderBlockFragment())
wanth = [][2]string{
{":status", "200"},
{"link", "</style.css>; rel=preload; as=style"},
{"link", "</script.js>; rel=preload; as=script"},
{"link", "</foo.js>; rel=preload; as=script"},
{"content-type", "text/plain; charset=utf-8"},
{"content-length", "5"},
}

if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Got = %q; want %q", goth, wanth)
}
})
}

func TestServerSendsProcessing(t *testing.T) {
testServerResponse(t, func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusProcessing)
w.Write([]byte("stuff"))

return nil
}, func(st *serverTester) {
getSlash(st)
hf := st.wantHeaders()
goth := st.decodeHeader(hf.HeaderBlockFragment())
wanth := [][2]string{
{":status", "102"},
}

if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Got = %q; want %q", goth, wanth)
}

hf = st.wantHeaders()
goth = st.decodeHeader(hf.HeaderBlockFragment())
wanth = [][2]string{
{":status", "200"},
{"content-type", "text/plain; charset=utf-8"},
{"content-length", "5"},
}

if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Got = %q; want %q", goth, wanth)
}
})
}

0 comments on commit 2f6bd1b

Please sign in to comment.