Skip to content

Commit 4e1c745

Browse files
committed
internal/http3: make Server response include headers that can be inferred
This CL makes Server automatically set certain headers in a response when they can be inferred and have not been explicitly set in a handler. For now, "Date" and "Content-Type" headers are supported. Note that unlike HTTP/1 and HTTP/2, "Content-Length" header is not automatically set. For golang/go#70914 Change-Id: Ie8d64ee0efb10118dfcba7a156f6f6886f565046 Reviewed-on: https://go-review.googlesource.com/c/net/+/746760 Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 19f580f commit 4e1c745

4 files changed

Lines changed: 463 additions & 54 deletions

File tree

internal/http3/body.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,34 @@ type bodyWriter struct {
4444
enc *qpackEncoder // QPACK encoder used by the connection.
4545
}
4646

47-
func (w *bodyWriter) Write(p []byte) (n int, err error) {
48-
if w.remain >= 0 && int64(len(p)) > w.remain {
47+
func (w *bodyWriter) write(ps ...[]byte) (n int, err error) {
48+
var size int64
49+
for _, p := range ps {
50+
size += int64(len(p))
51+
}
52+
// If write is called with empty byte slices, just return instead of
53+
// sending out a DATA frame containing nothing.
54+
if size == 0 {
55+
return 0, nil
56+
}
57+
if w.remain >= 0 && size > w.remain {
4958
return 0, &streamError{
5059
code: errH3InternalError,
5160
message: w.name + " body longer than specified content length",
5261
}
5362
}
5463
w.st.writeVarint(int64(frameTypeData))
55-
w.st.writeVarint(int64(len(p)))
56-
n, err = w.st.Write(p)
57-
if w.remain >= 0 {
58-
w.remain -= int64(n)
64+
w.st.writeVarint(size)
65+
for _, p := range ps {
66+
var n2 int
67+
n2, err = w.st.Write(p)
68+
n += n2
69+
if w.remain >= 0 {
70+
w.remain -= int64(n)
71+
}
72+
if err != nil {
73+
break
74+
}
5975
}
6076
if w.flush && err == nil {
6177
err = w.st.Flush()
@@ -66,6 +82,10 @@ func (w *bodyWriter) Write(p []byte) (n int, err error) {
6682
return n, err
6783
}
6884

85+
func (w *bodyWriter) Write(p []byte) (n int, err error) {
86+
return w.write(p)
87+
}
88+
6989
func (w *bodyWriter) Close() error {
7090
if w.remain > 0 {
7191
return errors.New(w.name + " body shorter than specified content length")

internal/http3/server.go

Lines changed: 142 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strconv"
1414
"strings"
1515
"sync"
16+
"time"
1617

1718
"golang.org/x/net/http/httpguts"
1819
"golang.org/x/net/internal/httpcommon"
@@ -253,10 +254,11 @@ func (sc *serverConn) handleRequestStream(st *stream) error {
253254
defer req.Body.Close()
254255

255256
rw := &responseWriter{
256-
st: st,
257-
headers: make(http.Header),
258-
trailer: make(http.Header),
259-
isHeadResp: req.Method == "HEAD",
257+
st: st,
258+
headers: make(http.Header),
259+
trailer: make(http.Header),
260+
bb: make(bodyBuffer, 0, defaultBodyBufferCap),
261+
cannotHaveBody: req.Method == "HEAD",
260262
bw: &bodyWriter{
261263
st: st,
262264
remain: -1,
@@ -268,8 +270,18 @@ func (sc *serverConn) handleRequestStream(st *stream) error {
268270
defer rw.close()
269271
if reqInfo.NeedsContinue {
270272
req.Body.(*bodyReader).send100Continue = func() {
271-
rw.WriteHeader(http.StatusContinue)
272-
rw.Flush()
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()
273285
}
274286
}
275287

@@ -290,14 +302,31 @@ func (sc *serverConn) abort(err error) {
290302
}
291303
}
292304

305+
// responseCanHaveBody reports whether a given response status code permits a
306+
// body. See RFC 7230, section 3.3.
307+
func responseCanHaveBody(status int) bool {
308+
switch {
309+
case status >= 100 && status <= 199:
310+
return false
311+
case status == 204:
312+
return false
313+
case status == 304:
314+
return false
315+
}
316+
return true
317+
}
318+
293319
type responseWriter struct {
294-
st *stream
295-
bw *bodyWriter
296-
mu sync.Mutex
297-
headers http.Header
298-
trailer http.Header
299-
wroteHeader bool // Non-1xx header has been (logically) written.
300-
isHeadResp bool // response is for a HEAD request.
320+
st *stream
321+
bw *bodyWriter
322+
mu sync.Mutex
323+
headers http.Header
324+
trailer http.Header
325+
bb bodyBuffer
326+
wroteHeader bool // Non-1xx header has been (logically) written.
327+
statusCode int // Status of the response that will be sent in HEADERS frame.
328+
statusCodeSet bool // Status of the response has been set via a call to WriteHeader.
329+
cannotHaveBody bool // Response should not have a body (e.g. response to a HEAD request).
301330
}
302331

303332
func (rw *responseWriter) Header() http.Header {
@@ -322,11 +351,13 @@ func (rw *responseWriter) prepareTrailerForWriteLocked() {
322351

323352
// Caller must hold rw.mu. If rw.wroteHeader is true, calling this method is a
324353
// no-op.
325-
func (rw *responseWriter) writeHeaderLockedOnce(statusCode int) {
354+
func (rw *responseWriter) writeHeaderLockedOnce() {
326355
if rw.wroteHeader {
327356
return
328357
}
329-
358+
if !responseCanHaveBody(rw.statusCode) {
359+
rw.cannotHaveBody = true
360+
}
330361
// If there is any Trailer declared in headers, save them so we know which
331362
// trailers have been pre-declared. Also, write back the extracted value,
332363
// which is canonicalized, to rw.Header for consistency.
@@ -335,10 +366,9 @@ func (rw *responseWriter) writeHeaderLockedOnce(statusCode int) {
335366
rw.headers.Set("Trailer", strings.Join(slices.Sorted(maps.Keys(rw.trailer)), ", "))
336367
}
337368

338-
enc := &qpackEncoder{}
339-
enc.init()
340-
encHeaders := enc.encode(func(f func(itype indexType, name, value string)) {
341-
f(mayIndex, ":status", strconv.Itoa(statusCode))
369+
rw.bb.inferHeader(rw.headers, rw.statusCode)
370+
encHeaders := rw.bw.enc.encode(func(f func(itype indexType, name, value string)) {
371+
f(mayIndex, ":status", strconv.Itoa(rw.statusCode))
342372
for name, values := range rw.headers {
343373
if !httpguts.ValidHeaderFieldName(name) {
344374
continue
@@ -352,45 +382,128 @@ func (rw *responseWriter) writeHeaderLockedOnce(statusCode int) {
352382
}
353383
}
354384
})
385+
355386
rw.st.writeVarint(int64(frameTypeHeaders))
356387
rw.st.writeVarint(int64(len(encHeaders)))
357388
rw.st.Write(encHeaders)
358-
if statusCode >= http.StatusOK {
389+
if rw.statusCode >= http.StatusOK {
359390
rw.wroteHeader = true
360391
}
361392
}
362393

363394
func (rw *responseWriter) WriteHeader(statusCode int) {
395+
// TODO: handle sending informational status headers (e.g. 103).
364396
rw.mu.Lock()
365397
defer rw.mu.Unlock()
366-
rw.writeHeaderLockedOnce(statusCode)
398+
if rw.statusCodeSet {
399+
return
400+
}
401+
rw.statusCodeSet = true
402+
rw.statusCode = statusCode
367403
}
368404

369405
func (rw *responseWriter) Write(b []byte) (int, error) {
406+
// Calling Write implicitly calls WriteHeader(200) if WriteHeader has not
407+
// been called before.
408+
rw.WriteHeader(http.StatusOK)
370409
rw.mu.Lock()
371410
defer rw.mu.Unlock()
372-
rw.writeHeaderLockedOnce(http.StatusOK)
373-
if rw.isHeadResp {
374-
return 0, nil
411+
412+
// If b fits entirely in our body buffer, save it to the buffer and return
413+
// early so we can coalesce small writes.
414+
// As a special case, we always want to save b to the buffer even when b is
415+
// big if we had yet to write our header, so we can infer headers like
416+
// "Content-Type" with as much information as possible.
417+
initialBufLen := len(rw.bb)
418+
if !rw.wroteHeader || len(b) <= cap(rw.bb)-len(rw.bb) {
419+
b = rw.bb.write(b)
420+
if len(b) == 0 {
421+
return len(b), nil
422+
}
423+
}
424+
425+
// Reaching this point means that our buffer has been sufficiently filled.
426+
// Therefore, we now want to:
427+
// 1. Infer and write response headers based on our body buffer, if not
428+
// done yet.
429+
// 2. Write our body buffer and the rest of b (if any).
430+
// 3. Reset the current body buffer so it can be used again.
431+
rw.writeHeaderLockedOnce()
432+
if rw.cannotHaveBody {
433+
return len(b), nil
434+
}
435+
if n, err := rw.bw.write(rw.bb, b); err != nil {
436+
return max(0, n-initialBufLen), err
375437
}
376-
return rw.bw.Write(b)
438+
rw.bb.discard()
439+
return len(b), nil
377440
}
378441

379442
func (rw *responseWriter) Flush() {
443+
// Calling Flush implicitly calls WriteHeader(200) if WriteHeader has not
444+
// been called before.
445+
rw.WriteHeader(http.StatusOK)
380446
rw.mu.Lock()
381-
rw.writeHeaderLockedOnce(http.StatusOK)
447+
rw.writeHeaderLockedOnce()
448+
if !rw.cannotHaveBody {
449+
rw.bw.Write(rw.bb)
450+
rw.bb.discard()
451+
}
382452
rw.mu.Unlock()
383-
rw.bw.st.Flush()
453+
rw.st.Flush()
384454
}
385455

386456
func (rw *responseWriter) close() error {
457+
rw.Flush()
387458
rw.mu.Lock()
388459
defer rw.mu.Unlock()
389-
rw.writeHeaderLockedOnce(http.StatusOK)
390460
rw.prepareTrailerForWriteLocked()
391-
392461
if err := rw.bw.Close(); err != nil {
393462
return err
394463
}
395464
return rw.st.stream.Close()
396465
}
466+
467+
// defaultBodyBufferCap is the default number of bytes of body that we are
468+
// willing to save in a buffer for the sake of inferring headers and coalescing
469+
// small writes. 512 was chosen to be consistent with how much
470+
// http.DetectContentType is willing to read.
471+
const defaultBodyBufferCap = 512
472+
473+
// bodyBuffer is a buffer used to store body content of a response.
474+
type bodyBuffer []byte
475+
476+
// write writes b to the buffer. It returns a new slice of b, which contains
477+
// any remaining data that could not be written to the buffer, if any.
478+
func (bb *bodyBuffer) write(b []byte) []byte {
479+
n := min(len(b), cap(*bb)-len(*bb))
480+
*bb = append(*bb, b[:n]...)
481+
return b[n:]
482+
}
483+
484+
// discard resets the buffer so it can be used again.
485+
func (bb *bodyBuffer) discard() {
486+
*bb = (*bb)[:0]
487+
}
488+
489+
// inferHeader populates h with the header values that we can infer from our
490+
// current buffer content, if not already explicitly set. This method should be
491+
// called only once with as much body content as possible in the buffer, before
492+
// a HEADERS frame is sent, and before discard has been called. Doing so
493+
// properly is the responsibility of the caller.
494+
func (bb *bodyBuffer) inferHeader(h http.Header, status int) {
495+
if _, ok := h["Date"]; !ok {
496+
h.Set("Date", time.Now().UTC().Format(http.TimeFormat))
497+
}
498+
// If the Content-Encoding is non-blank, we shouldn't
499+
// sniff the body. See Issue golang.org/issue/31753.
500+
_, hasCE := h["Content-Encoding"]
501+
_, hasCT := h["Content-Type"]
502+
if !hasCE && !hasCT && responseCanHaveBody(status) && len(*bb) > 0 {
503+
h.Set("Content-Type", http.DetectContentType(*bb))
504+
}
505+
// We can technically infer Content-Length too here, as long as the entire
506+
// response body fits within hi.buf and does not require flushing. However,
507+
// we have chosen not to do so for now as Content-Length is not very
508+
// important for HTTP/3, and such inconsistent behavior might be confusing.
509+
}

0 commit comments

Comments
 (0)