From eb3e23717ffe2cf4b54ff75e4f2773a145638e6e Mon Sep 17 00:00:00 2001 From: roc Date: Thu, 2 Jul 2026 12:02:13 +0800 Subject: [PATCH 1/3] port quic-go v0.60.0: fix QuarterStreamID field rename quic-go v0.60.0 corrected the qlog field spelling from QuaterStreamID to QuarterStreamID (PR #5590). Update internal/http3/conn.go to match. Since quic-go v0.60.0 requires Go 1.25+, bump go.mod directive to 1.25.0 and update CI matrix to test 1.25.x and 1.26.x. Fixes #500 --- .github/workflows/ci.yml | 2 +- go.mod | 12 ++++++------ go.sum | 10 ++++++++++ internal/http3/conn.go | 6 +++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f909beb..41b7986a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: test: strategy: matrix: - go: ['1.24.x', '1.25.x'] + go: ['1.25.x', '1.26.x'] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: diff --git a/go.mod b/go.mod index 43db6989..878a5d4b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/imroc/req/v3 -go 1.24.0 +go 1.25.0 require ( github.com/andybalholm/brotli v1.2.0 @@ -8,15 +8,15 @@ require ( github.com/icholy/digest v1.1.0 github.com/klauspost/compress v1.18.2 github.com/quic-go/qpack v0.6.0 - github.com/quic-go/quic-go v0.59.0 + github.com/quic-go/quic-go v0.60.0 github.com/refraction-networking/utls v1.8.2 - golang.org/x/net v0.48.0 - golang.org/x/text v0.32.0 + golang.org/x/net v0.55.0 + golang.org/x/text v0.37.0 ) require ( github.com/google/go-cmp v0.7.0 // indirect go.uber.org/mock v0.6.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/sys v0.45.0 // indirect ) diff --git a/go.sum b/go.sum index 8f5ff4a9..3d9058e6 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -27,12 +29,20 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/http3/conn.go b/internal/http3/conn.go index b1f09c23..d74e9d5b 100644 --- a/internal/http3/conn.go +++ b/internal/http3/conn.go @@ -375,7 +375,7 @@ func (c *Conn) sendDatagram(streamID quic.StreamID, b []byte) error { data = append(data, b...) if c.qlogger != nil { c.qlogger.RecordEvent(qlog.DatagramCreated{ - QuaterStreamID: quarterStreamID, + QuarterStreamID: quarterStreamID, Raw: qlog.RawInfo{ Length: len(data), PayloadLength: len(b), @@ -397,8 +397,8 @@ func (c *Conn) receiveDatagrams() error { return fmt.Errorf("could not read quarter stream id: %w", err) } if c.qlogger != nil { - c.qlogger.RecordEvent(qlog.DatagramParsed{ - QuaterStreamID: quarterStreamID, + c.qlogger.RecordEvent(qlog.DatagramParsed{ + QuarterStreamID: quarterStreamID, Raw: qlog.RawInfo{ Length: len(b), PayloadLength: len(b) - n, From 4c460fdc2db7287815134df4ea5e17b907304b90 Mon Sep 17 00:00:00 2001 From: roc Date: Thu, 2 Jul 2026 12:16:37 +0800 Subject: [PATCH 2/3] Sync remaining http3 changes from quic-go v0.60.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beyond the QuarterStreamID field rename, sync all other http3 changes from quic-go v0.59.0 → v0.60.0: headers.go: - Extract validateHeaderFieldNameAndValue, validateRegularHeaderField, validateTrailerHeaderField helper functions - parseTrailers: add sizeLimit parameter with per-field size accounting and errHeaderTooLarge check - Add validateTrailerHeaderField check in parseTrailers - Add validExtendedConnectProtocol function (RFC 9220) - Use strings.SplitSeq in extractAnnouncedTrailers - Add RFC 9220 comment on :protocol pseudo header request_writer.go: - Validate extended CONNECT :protocol using validExtendedConnectProtocol conn.go: - Update decodeTrailers to pass maxHeaderBytes as sizeLimit and use conditional headerFields pointer for parseTrailers Not synced (not applicable to req's client-only vendored http3): - conn.go qloggerWG.Go refactor (req removed qloggerWG WaitGroup) - server.go wg.Go refactor (req has no server logic) - response_writer.go changes (req has no response_writer.go) --- internal/http3/conn.go | 4 +- internal/http3/headers.go | 82 ++++++++++++++++++++++++-------- internal/http3/request_writer.go | 3 ++ 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/internal/http3/conn.go b/internal/http3/conn.go index d74e9d5b..5deae955 100644 --- a/internal/http3/conn.go +++ b/internal/http3/conn.go @@ -216,10 +216,12 @@ func (c *Conn) decodeTrailers(r io.Reader, streamID quic.StreamID, hf *headersFr } decodeFn := c.decoder.Decode(b) var fields []qpack.HeaderField + var headerFields *[]qpack.HeaderField if c.qlogger != nil { fields = make([]qpack.HeaderField, 0, 16) + headerFields = &fields } - trailers, err := parseTrailers(decodeFn, &fields) + trailers, err := parseTrailers(decodeFn, maxHeaderBytes, headerFields) if err != nil { maybeQlogInvalidHeadersFrame(c.qlogger, streamID, hf.Length) return nil, err diff --git a/internal/http3/headers.go b/internal/http3/headers.go index 572ca74c..02152742 100644 --- a/internal/http3/headers.go +++ b/internal/http3/headers.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/textproto" + "slices" "strconv" "strings" @@ -73,12 +74,8 @@ func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, sizeLimit int, head if sizeLimit < 0 { return header{}, errHeaderTooLarge } - // field names need to be lowercase, see section 4.2 of RFC 9114 - if strings.ToLower(h.Name) != h.Name { - return header{}, fmt.Errorf("header field is not lower-case: %s", h.Name) - } - if !httpguts.ValidHeaderFieldValue(h.Value) { - return header{}, fmt.Errorf("invalid header field value for %s: %q", h.Name, h.Value) + if err := validateHeaderFieldNameAndValue(h); err != nil { + return header{}, err } if h.IsPseudo() { if readFirstRegularHeader { @@ -97,7 +94,7 @@ func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, sizeLimit int, head case ":authority": isDuplicatePseudoHeader = hdr.Authority != "" hdr.Authority = h.Value - case ":protocol": + case ":protocol": // RFC 9220 isDuplicatePseudoHeader = hdr.Protocol != "" hdr.Protocol = h.Value case ":scheme": @@ -120,16 +117,8 @@ func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, sizeLimit int, head return header{}, fmt.Errorf("invalid response pseudo header: %s", h.Name) } } else { - if !httpguts.ValidHeaderFieldName(h.Name) { - return header{}, fmt.Errorf("invalid header field name: %q", h.Name) - } - for _, invalidField := range invalidHeaderFields { - if h.Name == invalidField { - return header{}, fmt.Errorf("invalid header field name: %q", h.Name) - } - } - if h.Name == "te" && h.Value != "trailers" { - return header{}, fmt.Errorf("invalid TE header field value: %q", h.Value) + if err := validateRegularHeaderField(h); err != nil { + return header{}, err } readFirstRegularHeader = true switch h.Name { @@ -163,7 +152,41 @@ func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, sizeLimit int, head return hdr, nil } -func parseTrailers(decodeFn qpack.DecodeFunc, headerFields *[]qpack.HeaderField) (http.Header, error) { +func validateHeaderFieldNameAndValue(h qpack.HeaderField) error { + // field names need to be lowercase, see section 4.2 of RFC 9114 + if strings.ToLower(h.Name) != h.Name { + return fmt.Errorf("header field is not lower-case: %s", h.Name) + } + if !httpguts.ValidHeaderFieldValue(h.Value) { + return fmt.Errorf("invalid header field value for %s: %q", h.Name, h.Value) + } + return nil +} + +func validateRegularHeaderField(h qpack.HeaderField) error { + if !httpguts.ValidHeaderFieldName(h.Name) { + return fmt.Errorf("invalid header field name: %q", h.Name) + } + if slices.Contains(invalidHeaderFields[:], h.Name) { + return fmt.Errorf("invalid header field name: %q", h.Name) + } + if h.Name == "te" && h.Value != "trailers" { + return fmt.Errorf("invalid TE header field value: %q", h.Value) + } + return nil +} + +func validateTrailerHeaderField(h qpack.HeaderField) error { + if err := validateRegularHeaderField(h); err != nil { + return err + } + if !httpguts.ValidTrailerHeader(h.Name) { + return fmt.Errorf("invalid trailer field name: %q", h.Name) + } + return nil +} + +func parseTrailers(decodeFn qpack.DecodeFunc, sizeLimit int, headerFields *[]qpack.HeaderField) (http.Header, error) { h := make(http.Header) for { hf, err := decodeFn() @@ -176,14 +199,35 @@ func parseTrailers(decodeFn qpack.DecodeFunc, headerFields *[]qpack.HeaderField) if headerFields != nil { *headerFields = append(*headerFields, hf) } + // RFC 9114, section 4.2.2: + // The size of a field list is calculated based on the uncompressed size of fields, + // including the length of the name and value in bytes plus an overhead of 32 bytes for each field. + sizeLimit -= len(hf.Name) + len(hf.Value) + 32 + if sizeLimit < 0 { + return nil, errHeaderTooLarge + } + if err := validateHeaderFieldNameAndValue(hf); err != nil { + return nil, err + } if hf.IsPseudo() { return nil, fmt.Errorf("http3: received pseudo header in trailer: %s", hf.Name) } + if err := validateTrailerHeaderField(hf); err != nil { + return nil, err + } h.Add(hf.Name, hf.Value) } return h, nil } +func validExtendedConnectProtocol(protocol string) bool { + // RFC 9220 specifies that the semantics of the :protocol pseudo are the same as defined in RFC 8441. + // RFC 8441, Section 4 specifies that :protocol is a single value from the HTTP Upgrade Token Registry. + // RFC 9110, Section 16.7 specifies that HTTP Upgrade Token Registry uses token grammar. + // Therefore, ValidHeaderFieldName is the right syntax check here, despite the misleading name. + return httpguts.ValidHeaderFieldName(protocol) +} + // updateResponseFromHeaders sets up http.Response as an HTTP/3 response, // using the decoded qpack header filed. // It is only called for the HTTP header (and not the HTTP trailer). @@ -228,7 +272,7 @@ func extractAnnouncedTrailers(header http.Header) http.Header { trailers := make(http.Header) for _, rawVal := range rawTrailers { - for _, val := range strings.Split(rawVal, ",") { + for val := range strings.SplitSeq(rawVal, ",") { trailers[http.CanonicalHeaderKey(textproto.TrimString(val))] = nil } } diff --git a/internal/http3/request_writer.go b/internal/http3/request_writer.go index fbf0c5e1..8e879ab8 100644 --- a/internal/http3/request_writer.go +++ b/internal/http3/request_writer.go @@ -113,6 +113,9 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra // http.NewRequest sets this field to HTTP/1.1 isExtendedConnect := isExtendedConnectRequest(req) + if isExtendedConnect && !validExtendedConnectProtocol(req.Proto) { + return nil, fmt.Errorf("invalid request :protocol %q", req.Proto) + } var path string if req.Method != http.MethodConnect || isExtendedConnect { From 6fd255412995b7edc982f150d05080b3d9e6bc55 Mon Sep 17 00:00:00 2001 From: roc Date: Thu, 2 Jul 2026 12:20:28 +0800 Subject: [PATCH 3/3] docs: add mandatory upstream sync checklist to CODEBUDDY.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add explicit step-by-step process for syncing upstream changes (quic-go / net/http / http2) to prevent incomplete syncs where only the compile error is fixed but other upstream changes are missed. Key addition: 'compilation passes ≠ sync complete' — must diff all non-test files, sync each one, and explicitly document skipped items. --- .codebuddy/CODEBUDDY.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.codebuddy/CODEBUDDY.md b/.codebuddy/CODEBUDDY.md index ffadd2dd..25208f5a 100644 --- a/.codebuddy/CODEBUDDY.md +++ b/.codebuddy/CODEBUDDY.md @@ -5,7 +5,7 @@ ## 基本信息 - **模块路径**: `github.com/imroc/req/v3` -- **Go 版本**: 1.24+(CI 测试 1.24.x 和 1.25.x) +- **Go 版本**: 1.25+(CI 测试 1.25.x 和 1.26.x) - **主分支**: `master` - **包管理**: Go modules(`go.mod` / `go.sum`) @@ -23,6 +23,16 @@ - 固定更新到 Go 标准库 HTTP 最新稳定版 + quic-go 最新稳定版 - 提交信息惯例:`merge upstream net/http: <日期>()`、`merge upstream http2: <日期>()`、`port quic-go <版本>` +### 同步上游变更的强制流程(quic-go / net/http / http2 通用) + +> **禁止只修复编译报错就提交**。编译通过 ≠ 同步完整。必须完整执行以下步骤: + +1. **全量 diff**:对比新旧版本所有非测试文件(`diff -rq` 列出差异文件,再逐一 `diff` 非测试文件) +2. **逐文件同步**:每个有差异的非测试文件,都要将上游变更合并到 req 对应的魔改文件,保留 req 定制逻辑(如 dump、middleware、header order 等) +3. **显式记录不适用项**:对于 req 不需要的上游变更(如 server 端逻辑),必须在 PR 描述中列出并说明原因,证明做过全面检查而非遗漏 +4. **全量测试**:`go build ./...` + `go test ./...` 全部通过 +5. **PR 描述包含同步清单**:列出每个同步的文件和变更点,以及不适用项 + ## 常用命令 ```bash