http2: 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 golang/go#26089.
Fixes golang/go#36734.
Updates golang/go#26088.

Change-Id: Iadb7be92844a3588ef4ce044f887ef5c1792f431
GitHub-Last-Rev: eee95b58b3b259781fc1052ed6143a0aa9284e04
GitHub-Pull-Request: golang/net#96
Reviewed-on: https://go-review.googlesource.com/c/net/+/291029
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/http2/server.go b/http2/server.go
index 4bb0d66..5e2583c 100644
--- a/http2/server.go
+++ b/http2/server.go
@@ -2645,8 +2645,7 @@
 	// 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
@@ -2667,13 +2666,33 @@
 }
 
 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
+	if code >= 100 && code <= 199 {
+		// 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)
 	}
 }
 
diff --git a/http2/server_test.go b/http2/server_test.go
index 46ac6ee..ef5ad5b 100644
--- a/http2/server_test.go
+++ b/http2/server_test.go
@@ -4356,3 +4356,92 @@
 		t.Error("got protocol error")
 	}
 }
+
+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)
+		}
+	})
+}
+
+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)
+		}
+	})
+}