http3: add basic client-side qlog support (#5374)

This commit is contained in:
Marten Seemann
2025-10-11 16:13:49 +08:00
committed by GitHub
parent 53d246840d
commit 7796175c64
6 changed files with 105 additions and 28 deletions

View File

@@ -17,6 +17,9 @@ import (
"golang.org/x/net/idna"
"github.com/quic-go/qpack"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3/qlog"
"github.com/quic-go/quic-go/qlogwriter"
)
const bodyCopyBufferSize = 8 * 1024
@@ -36,10 +39,10 @@ func newRequestWriter() *requestWriter {
}
}
func (w *requestWriter) WriteRequestHeader(wr io.Writer, req *http.Request, gzip bool) error {
func (w *requestWriter) WriteRequestHeader(wr io.Writer, req *http.Request, gzip bool, streamID quic.StreamID, qlogger qlogwriter.Recorder) error {
// TODO: figure out how to add support for trailers
buf := &bytes.Buffer{}
if err := w.writeHeaders(buf, req, gzip); err != nil {
if err := w.writeHeaders(buf, req, gzip, streamID, qlogger); err != nil {
return err
}
if _, err := wr.Write(buf.Bytes()); err != nil {
@@ -50,22 +53,26 @@ func (w *requestWriter) WriteRequestHeader(wr io.Writer, req *http.Request, gzip
return nil
}
func (w *requestWriter) writeHeaders(wr io.Writer, req *http.Request, gzip bool) error {
func (w *requestWriter) writeHeaders(wr io.Writer, req *http.Request, gzip bool, streamID quic.StreamID, qlogger qlogwriter.Recorder) error {
w.mutex.Lock()
defer w.mutex.Unlock()
defer w.encoder.Close()
defer w.headerBuf.Reset()
if err := w.encodeHeaders(req, gzip, "", actualContentLength(req)); err != nil {
headerFields, err := w.encodeHeaders(req, gzip, "", actualContentLength(req), qlogger != nil)
if err != nil {
return err
}
b := make([]byte, 0, 128)
b = (&headersFrame{Length: uint64(w.headerBuf.Len())}).Append(b)
if qlogger != nil {
qlogCreatedHeadersFrame(qlogger, streamID, len(b)+w.headerBuf.Len(), w.headerBuf.Len(), headerFields)
}
if _, err := wr.Write(b); err != nil {
return err
}
_, err := wr.Write(w.headerBuf.Bytes())
_, err = wr.Write(w.headerBuf.Bytes())
return err
}
@@ -77,17 +84,19 @@ func isExtendedConnectRequest(req *http.Request) bool {
// Modified to support Extended CONNECT:
// Contrary to what the godoc for the http.Request says,
// we do respect the Proto field if the method is CONNECT.
func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) error {
//
// The returned header fields are only set if doQlog is true.
func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64, doQlog bool) ([]qlog.HeaderField, error) {
host := req.Host
if host == "" {
host = req.URL.Host
}
host, err := httpguts.PunycodeHostPort(host)
if err != nil {
return err
return nil, err
}
if !httpguts.ValidHostHeader(host) {
return errors.New("http3: invalid Host header")
return nil, errors.New("http3: invalid Host header")
}
// http.NewRequest sets this field to HTTP/1.1
@@ -101,9 +110,9 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra
path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host)
if !validPseudoPath(path) {
if req.URL.Opaque != "" {
return fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
return nil, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
} else {
return fmt.Errorf("invalid request :path %q", orig)
return nil, fmt.Errorf("invalid request :path %q", orig)
}
}
}
@@ -114,11 +123,11 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra
// continue to reuse the hpack encoder for future requests)
for k, vv := range req.Header {
if !httpguts.ValidHeaderFieldName(k) {
return fmt.Errorf("invalid HTTP header name %q", k)
return nil, fmt.Errorf("invalid HTTP header name %q", k)
}
for _, v := range vv {
if !httpguts.ValidHeaderFieldValue(v) {
return fmt.Errorf("invalid HTTP header value %q for header %q", v, k)
return nil, fmt.Errorf("invalid HTTP header value %q for header %q", v, k)
}
}
}
@@ -206,15 +215,22 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra
traceHeaders := traceHasWroteHeaderField(trace)
// Header list size is ok. Write the headers.
var headerFields []qlog.HeaderField
if doQlog {
headerFields = make([]qlog.HeaderField, 0, len(req.Header))
}
enumerateHeaders(func(name, value string) {
name = strings.ToLower(name)
w.encoder.WriteField(qpack.HeaderField{Name: name, Value: value})
if traceHeaders {
traceWroteHeaderField(trace, name, value)
}
if doQlog {
headerFields = append(headerFields, qlog.HeaderField{Name: name, Value: value})
}
})
return nil
return headerFields, nil
}
// authorityAddr returns a given authority (a host/IP, or host:port / ip:port)