Skip to content

Commit 591bdf3

Browse files
committed
internal/http3: add HTTP 103 Early Hints support to Server
Using WriteHeader from Server handler to send status 103 will now work similarly to HTTP/1 and HTTP/2. Additionally, the code path for handling informational response header in general has also been simplified. Support for the client is added in a separate change. For golang/go#70914 Change-Id: I07bd15fc56d0b1e18f1831c9002d548dbfb5beb5 Reviewed-on: https://go-review.googlesource.com/c/net/+/749264 Reviewed-by: Damien Neil <dneil@google.com> Reviewed-by: Nicholas Husin <husin@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 1faa6d8 commit 591bdf3

File tree

2 files changed

+98
-17
lines changed

2 files changed

+98
-17
lines changed

internal/http3/server.go

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -270,18 +270,7 @@ func (sc *serverConn) handleRequestStream(st *stream) error {
270270
defer rw.close()
271271
if reqInfo.NeedsContinue {
272272
req.Body.(*bodyReader).send100Continue = func() {
273-
rw.mu.Lock()
274-
defer rw.mu.Unlock()
275-
if rw.wroteHeader {
276-
return
277-
}
278-
encHeaders := rw.bw.enc.encode(func(f func(itype indexType, name, value string)) {
279-
f(mayIndex, ":status", strconv.Itoa(http.StatusContinue))
280-
})
281-
rw.st.writeVarint(int64(frameTypeHeaders))
282-
rw.st.writeVarint(int64(len(encHeaders)))
283-
rw.st.Write(encHeaders)
284-
rw.st.Flush()
273+
rw.WriteHeader(100)
285274
}
286275
}
287276

@@ -350,8 +339,10 @@ func (rw *responseWriter) prepareTrailerForWriteLocked() {
350339
}
351340
}
352341

353-
// Caller must hold rw.mu. If rw.wroteHeader is true, calling this method is a
354-
// no-op.
342+
// writeHeaderLockedOnce writes the final response header. If rw.wroteHeader is
343+
// true, calling this method is a no-op. Sending informational status headers
344+
// should be done using writeInfoHeaderLocked, rather than this method.
345+
// Caller must hold rw.mu.
355346
func (rw *responseWriter) writeHeaderLockedOnce() {
356347
if rw.wroteHeader {
357348
return
@@ -387,9 +378,42 @@ func (rw *responseWriter) writeHeaderLockedOnce() {
387378
rw.st.writeVarint(int64(frameTypeHeaders))
388379
rw.st.writeVarint(int64(len(encHeaders)))
389380
rw.st.Write(encHeaders)
390-
if rw.statusCode >= http.StatusOK {
391-
rw.wroteHeader = true
381+
rw.wroteHeader = true
382+
}
383+
384+
// writeHeaderLocked writes informational status headers (i.e. status 1XX).
385+
// If a non-informational status header has been written via
386+
// writeHeaderLockedOnce, this method is a no-op.
387+
// Caller must hold rw.mu.
388+
func (rw *responseWriter) writeHeaderLocked(statusCode int) {
389+
if rw.wroteHeader {
390+
return
392391
}
392+
encHeaders := rw.bw.enc.encode(func(f func(itype indexType, name, value string)) {
393+
f(mayIndex, ":status", strconv.Itoa(statusCode))
394+
for name, values := range rw.headers {
395+
if name == "Content-Length" || name == "Transfer-Encoding" {
396+
continue
397+
}
398+
if !httpguts.ValidHeaderFieldName(name) {
399+
continue
400+
}
401+
for _, val := range values {
402+
if !httpguts.ValidHeaderFieldValue(val) {
403+
continue
404+
}
405+
// Issue #71374: Consider supporting never-indexed fields.
406+
f(mayIndex, name, val)
407+
}
408+
}
409+
})
410+
rw.st.writeVarint(int64(frameTypeHeaders))
411+
rw.st.writeVarint(int64(len(encHeaders)))
412+
rw.st.Write(encHeaders)
413+
}
414+
415+
func isInfoStatus(status int) bool {
416+
return status >= 100 && status < 200
393417
}
394418

395419
func (rw *responseWriter) WriteHeader(statusCode int) {
@@ -399,9 +423,19 @@ func (rw *responseWriter) WriteHeader(statusCode int) {
399423
if rw.statusCodeSet {
400424
return
401425
}
426+
427+
// Informational headers can be sent multiple times, and should be flushed
428+
// immediately.
429+
if isInfoStatus(statusCode) {
430+
rw.writeHeaderLocked(statusCode)
431+
rw.st.Flush()
432+
return
433+
}
434+
435+
// Non-informational headers should only be set once, and should be
436+
// buffered.
402437
rw.statusCodeSet = true
403438
rw.statusCode = statusCode
404-
405439
if n, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil {
406440
rw.bodyLenLeft = n
407441
} else {

internal/http3/server_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,53 @@ func TestServerBuffersBodyWrite(t *testing.T) {
865865
}
866866
}
867867

868+
func TestServer103EarlyHints(t *testing.T) {
869+
synctest.Test(t, func(t *testing.T) {
870+
body := []byte("some body")
871+
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
872+
h := w.Header()
873+
874+
h.Add("Content-Length", "123") // Must be ignored
875+
h.Add("Link", "</style.css>; rel=preload; as=style")
876+
h.Add("Link", "</script.js>; rel=preload; as=script")
877+
w.WriteHeader(http.StatusEarlyHints)
878+
879+
h.Add("Link", "</foo.js>; rel=preload; as=script")
880+
w.WriteHeader(http.StatusEarlyHints)
881+
882+
w.Write(body) // Implicitly sends status 200.
883+
w.WriteHeader(http.StatusEarlyHints) // Should be a no-op.
884+
}))
885+
tc := ts.connect()
886+
tc.greet()
887+
888+
reqStream := tc.newStream(streamTypeRequest)
889+
reqStream.writeHeaders(requestHeader(nil))
890+
synctest.Wait()
891+
reqStream.wantHeaders(http.Header{
892+
":status": {"103"},
893+
"Link": {
894+
"</style.css>; rel=preload; as=style",
895+
"</script.js>; rel=preload; as=script",
896+
},
897+
})
898+
reqStream.wantHeaders(http.Header{
899+
":status": {"103"},
900+
"Link": {
901+
"</style.css>; rel=preload; as=style",
902+
"</script.js>; rel=preload; as=script",
903+
"</foo.js>; rel=preload; as=script",
904+
},
905+
})
906+
reqStream.wantSomeHeaders(http.Header{
907+
":status": {"200"},
908+
"Content-Length": {"123"},
909+
})
910+
reqStream.wantData(body)
911+
reqStream.wantClosed("request is complete")
912+
})
913+
}
914+
868915
type testServer struct {
869916
t testing.TB
870917
s *Server

0 commit comments

Comments
 (0)