// Copyright 2025 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package httpcommonimport ()var (ErrRequestHeaderListSize = errors.New("request header list larger than peer's advertised limit"))// Request is a subset of http.Request.// It'd be simpler to pass an *http.Request, of course, but we can't depend on net/http// without creating a dependency cycle.typeRequeststruct {URL *url.URLMethodstringHoststringHeadermap[string][]stringTrailermap[string][]stringActualContentLengthint64// 0 means 0, -1 means unknown}// EncodeHeadersParam is parameters to EncodeHeaders.typeEncodeHeadersParamstruct {RequestRequest// AddGzipHeader indicates that an "accept-encoding: gzip" header should be // added to the request.AddGzipHeaderbool// PeerMaxHeaderListSize, when non-zero, is the peer's MAX_HEADER_LIST_SIZE setting.PeerMaxHeaderListSizeuint64// DefaultUserAgent is the User-Agent header to send when the request // neither contains a User-Agent nor disables it.DefaultUserAgentstring}// EncodeHeadersParam is the result of EncodeHeaders.typeEncodeHeadersResultstruct {HasBodyboolHasTrailersbool}// EncodeHeaders constructs request headers common to HTTP/2 and HTTP/3.// It validates a request and calls headerf with each pseudo-header and header// for the request.// The headerf function is called with the validated, canonicalized header name.func ( context.Context, EncodeHeadersParam, func(, string)) ( EncodeHeadersResult, error) { := .Request// Check for invalid connection-level headers.if := checkConnHeaders(.Header); != nil {return , }if .URL == nil {return , errors.New("Request.URL is nil") } := .Hostif == "" { = .URL.Host } , := httpguts.PunycodeHostPort()if != nil {return , }if !httpguts.ValidHostHeader() {return , errors.New("invalid Host header") }// isNormalConnect is true if this is a non-extended CONNECT request. := falsevarstringif := .Header[":protocol"]; len() > 0 { = [0] }if .Method == "CONNECT" && == "" { = true } elseif != "" && .Method != "CONNECT" {return , errors.New("invalid :protocol header in non-CONNECT request") }// Validate the path, except for non-extended CONNECT requests which have no path.varstringif ! { = .URL.RequestURI()if !validPseudoPath() { := = strings.TrimPrefix(, .URL.Scheme+"://"+)if !validPseudoPath() {if .URL.Opaque != "" {return , fmt.Errorf("invalid request :path %q from URL.Opaque = %q", , .URL.Opaque) } else {return , fmt.Errorf("invalid request :path %q", ) } } } }// Check for any invalid headers+trailers and return an error before we // potentially pollute our hpack state. (We want to be able to // continue to reuse the hpack encoder for future requests)if := validateHeaders(.Header); != "" {return , fmt.Errorf("invalid HTTP header %s", ) }if := validateHeaders(.Trailer); != "" {return , fmt.Errorf("invalid HTTP trailer %s", ) } , := commaSeparatedTrailers(.Trailer)if != nil {return , } := func( func(, string)) {// 8.1.2.3 Request Pseudo-Header Fields // The :path pseudo-header field includes the path and query parts of the // target URI (the path-absolute production and optionally a '?' character // followed by the query production, see Sections 3.3 and 3.4 of // [RFC3986]). (":authority", ) := .Methodif == "" { = "GET" } (":method", )if ! { (":path", ) (":scheme", .URL.Scheme) }if != "" { (":protocol", ) }if != "" { ("trailer", ) }varboolfor , := range .Header {ifasciiEqualFold(, "host") || asciiEqualFold(, "content-length") {// Host is :authority, already sent. // Content-Length is automatic, set below.continue } elseifasciiEqualFold(, "connection") ||asciiEqualFold(, "proxy-connection") ||asciiEqualFold(, "transfer-encoding") ||asciiEqualFold(, "upgrade") ||asciiEqualFold(, "keep-alive") {// Per 8.1.2.2 Connection-Specific Header // Fields, don't send connection-specific // fields. We have already checked if any // are error-worthy so just ignore the rest.continue } elseifasciiEqualFold(, "user-agent") {// Match Go's http1 behavior: at most one // User-Agent. If set to nil or empty string, // then omit it. Otherwise if not mentioned, // include the default (below). = trueiflen() < 1 {continue } = [:1]if [0] == "" {continue } } elseifasciiEqualFold(, "cookie") {// Per 8.1.2.5 To allow for better compression efficiency, the // Cookie header field MAY be split into separate header fields, // each with one or more cookie-pairs.for , := range {for { := strings.IndexByte(, ';')if < 0 {break } ("cookie", [:]) ++// strip space after semicolon if any.for +1 <= len() && [] == ' ' { ++ } = [:] }iflen() > 0 { ("cookie", ) } }continue } elseif == ":protocol" {// :protocol pseudo-header was already sent above.continue }for , := range { (, ) } }ifshouldSendReqContentLength(.Method, .ActualContentLength) { ("content-length", strconv.FormatInt(.ActualContentLength, 10)) }if .AddGzipHeader { ("accept-encoding", "gzip") }if ! { ("user-agent", .DefaultUserAgent) } }// Do a first pass over the headers counting bytes to ensure // we don't exceed cc.peerMaxHeaderListSize. This is done as a // separate pass before encoding the headers to prevent // modifying the hpack state.if .PeerMaxHeaderListSize > 0 { := uint64(0) (func(, string) { := hpack.HeaderField{Name: , Value: } += uint64(.Size()) })if > .PeerMaxHeaderListSize {return , ErrRequestHeaderListSize } } := httptrace.ContextClientTrace()// Header list size is ok. Write the headers. (func(, string) { , := LowerHeader()if ! {// Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header // field names have to be ASCII characters (just as in HTTP/1.x).return } (, )if != nil && .WroteHeaderField != nil { .WroteHeaderField(, []string{}) } }) .HasBody = .ActualContentLength != 0 .HasTrailers = != ""return , nil}// IsRequestGzip reports whether we should add an Accept-Encoding: gzip header// for a request.func ( string, map[string][]string, bool) bool {// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?if ! &&len(["Accept-Encoding"]) == 0 &&len(["Range"]) == 0 && != "HEAD" {// Request gzip only, not deflate. Deflate is ambiguous and // not as universally supported anyway. // See: https://zlib.net/zlib_faq.html#faq39 // // Note that we don't request this for HEAD requests, // due to a bug in nginx: // http://trac.nginx.org/nginx/ticket/358 // https://golang.org/issue/5522 // // We don't request gzip if the request is for a range, since // auto-decoding a portion of a gzipped document will just fail // anyway. See https://golang.org/issue/8923returntrue }returnfalse}// checkConnHeaders checks whether req has any invalid connection-level headers.//// https://www.rfc-editor.org/rfc/rfc9114.html#section-4.2-3// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.2-1//// Certain headers are special-cased as okay but not transmitted later.// For example, we allow "Transfer-Encoding: chunked", but drop the header when encoding.func ( map[string][]string) error {if := ["Upgrade"]; len() > 0 && ([0] != "" && [0] != "chunked") {returnfmt.Errorf("invalid Upgrade request header: %q", ) }if := ["Transfer-Encoding"]; len() > 0 && (len() > 1 || [0] != "" && [0] != "chunked") {returnfmt.Errorf("invalid Transfer-Encoding request header: %q", ) }if := ["Connection"]; len() > 0 && (len() > 1 || [0] != "" && !asciiEqualFold([0], "close") && !asciiEqualFold([0], "keep-alive")) {returnfmt.Errorf("invalid Connection request header: %q", ) }returnnil}func ( map[string][]string) (string, error) { := make([]string, 0, len())for := range { = CanonicalHeader()switch {case"Transfer-Encoding", "Trailer", "Content-Length":return"", fmt.Errorf("invalid Trailer key %q", ) } = append(, ) }iflen() > 0 {sort.Strings()returnstrings.Join(, ","), nil }return"", nil}// validPseudoPath reports whether v is a valid :path pseudo-header// value. It must be either://// - a non-empty string starting with '/'// - the string '*', for OPTIONS requests.//// For now this is only used a quick check for deciding when to clean// up Opaque URLs before sending requests from the Transport.// See golang.org/issue/16847//// We used to enforce that the path also didn't start with "//", but// Google's GFE accepts such paths and Chrome sends them, so ignore// that part of the spec. See golang.org/issue/19103.func ( string) bool {return (len() > 0 && [0] == '/') || == "*"}func ( map[string][]string) string {for , := range {if !httpguts.ValidHeaderFieldName() && != ":protocol" {returnfmt.Sprintf("name %q", ) }for , := range {if !httpguts.ValidHeaderFieldValue() {// Don't include the value in the error, // because it may be sensitive.returnfmt.Sprintf("value for header %q", ) } } }return""}// shouldSendReqContentLength reports whether we should send// a "content-length" request header. This logic is basically a copy of the net/http// transferWriter.shouldSendContentLength.// The contentLength is the corrected contentLength (so 0 means actually 0, not unknown).// -1 means unknown.func ( string, int64) bool {if > 0 {returntrue }if < 0 {returnfalse }// For zero bodies, whether we send a content-length depends on the method. // It also kinda doesn't matter for http2 either way, with END_STREAM.switch {case"POST", "PUT", "PATCH":returntruedefault:returnfalse }}// ServerRequestParam is parameters to NewServerRequest.typeServerRequestParamstruct {MethodstringScheme, Authority, PathstringProtocolstringHeadermap[string][]string}// ServerRequestResult is the result of NewServerRequest.typeServerRequestResultstruct {// Various http.Request fields.URL *url.URLRequestURIstringTrailermap[string][]stringNeedsContinuebool// client provided an "Expect: 100-continue" header// If the request should be rejected, this is a short string suitable for passing // to the http2 package's CountError function. // It might be a bit odd to return errors this way rather than returing an error, // but this ensures we don't forget to include a CountError reason.InvalidReasonstring}func ( ServerRequestParam) ServerRequestResult { := httpguts.HeaderValuesContainsToken(.Header["Expect"], "100-continue")if {delete(.Header, "Expect") }// Merge Cookie headers into one "; "-delimited value.if := .Header["Cookie"]; len() > 1 { .Header["Cookie"] = []string{strings.Join(, "; ")} }// Setup Trailersvarmap[string][]stringfor , := range .Header["Trailer"] {for , := rangestrings.Split(, ",") { = textproto.CanonicalMIMEHeaderKey(textproto.TrimString())switch {case"Transfer-Encoding", "Trailer", "Content-Length":// Bogus. (copy of http1 rules) // Ignore.default:if == nil { = make(map[string][]string) } [] = nil } } }delete(.Header, "Trailer")// "':authority' MUST NOT include the deprecated userinfo subcomponent // for "http" or "https" schemed URIs." // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8ifstrings.IndexByte(.Authority, '@') != -1 && (.Scheme == "http" || .Scheme == "https") {returnServerRequestResult{InvalidReason: "userinfo_in_authority", } }var *url.URLvarstringif .Method == "CONNECT" && .Protocol == "" { = &url.URL{Host: .Authority} = .Authority// mimic HTTP/1 server behavior } else {varerror , = url.ParseRequestURI(.Path)if != nil {returnServerRequestResult{InvalidReason: "bad_path", } } = .Path }returnServerRequestResult{URL: ,NeedsContinue: ,RequestURI: ,Trailer: , }}
The pages are generated with Goldsv0.7.6. (GOOS=linux GOARCH=amd64)