From 6b54f74dd5617bbd70bcab3de922a0f51ccfa5f0 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Wed, 29 Jul 2026 16:26:20 -0300 Subject: [PATCH] add Go's ServeMux Request.PathValue access semantics to Exchange, Mux and MuxSlice --- http/httphi/exchange.go | 26 +++++ http/httphi/mux.go | 112 +++++++++++++++++-- http/httphi/mux_test.go | 222 ++++++++++++++++++++++++++++++++++++++ http/httphi/router.go | 7 ++ http/httpraw/form.go | 5 +- http/httpraw/form_test.go | 23 ++++ http/httpraw/header.go | 38 ------- http/httpraw/kvbuffer.go | 6 +- http/httpraw/parse.go | 8 +- 9 files changed, 394 insertions(+), 53 deletions(-) create mode 100644 http/httphi/mux_test.go diff --git a/http/httphi/exchange.go b/http/httphi/exchange.go index 447d424..e1148dd 100644 --- a/http/httphi/exchange.go +++ b/http/httphi/exchange.go @@ -34,6 +34,7 @@ type Exchange struct { respHeaderOff uint16 respHeaderLen uint16 reqHdr httpraw.Header + pathValues []pathValue hijacked bool rw conn @@ -54,6 +55,9 @@ type ExchangeConfig struct { NumHeaderKVCap int NormalizeOutgoingKeys bool NoRequestBufferGrowth bool + // MaxPathValues is how many wildcards a single pattern may bind, read back with + // [Exchange.PathValue]. A pattern with more never matches, see [SetPathValues]. + MaxPathValues int } // HijackRaw is a low-level implementation of http.Hijacker interface. @@ -99,6 +103,27 @@ func (exch *Exchange) Configure(cfg ExchangeConfig) { exch.reqHdr.Reset(cfg.RawBuf[:0:cfg.RequestBufferLim], cfg.NumHeaderKVCap) exch.reqHdr.ConfigBufferGrowth(!cfg.NoRequestBufferGrowth) exch.normalizeKeys = cfg.NormalizeOutgoingKeys + internal.SliceReuse(&exch.pathValues, cfg.MaxPathValues) + exch.pathValues = exch.pathValues[:cfg.MaxPathValues] +} + +// PathValue returns the segment the request path bound to the wildcard named +// key, or nil if the matched pattern has no such wildcard. It plays the part of +// http.Request.PathValue. See [SetPathValues] for the pattern syntax and for +// which segments a wildcard binds. +// +// sm.Handle("GET /users/{id}", func(exch *httphi.Exchange) { +// id := exch.PathValue("id") // "42" on a GET /users/42. +// }) +func (exch *Exchange) PathValue(key string) []byte { + for i := range exch.pathValues { + if exch.pathValues[i].Key == key { + return exch.pathValues[i].Value + } else if exch.pathValues[i].Key == "" { + break // No more keys set. + } + } + return nil } // Acquire claims the exchange for conn and resets it to serve a new request, @@ -120,6 +145,7 @@ func (exch *Exchange) Acquire(conn conn) bool { exch.rw = conn exch.headerWritten = false exch.nextFree = nil + clear(exch.pathValues) exch.reqHdr.Reset(nil, 0) return true } diff --git a/http/httphi/mux.go b/http/httphi/mux.go index f5c4c55..7ca7e6f 100644 --- a/http/httphi/mux.go +++ b/http/httphi/mux.go @@ -1,6 +1,7 @@ package httphi import ( + "bytes" "io" "strings" "unsafe" @@ -65,7 +66,7 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error { // Mux on the request path: the query string is the handler's business. path := reqhdr.RequestPath() meth := reqhdr.Method() - matchedPattern, handler := mux.LookupHandler(MethodFromBytes(meth), b2s(path)) + matchedPattern, handler := mux.LookupHandler(MethodFromBytes(meth), path, exch.pathValues) if handler != nil { exch.matchedPattern = matchedPattern handler(exch) @@ -98,8 +99,9 @@ type HandlerFunc func(ex *Exchange) // replies 404 when it returns nil. type Mux interface { // LookupHandler matches the requestPath and method to a handler and returns it and the - // pattern it matched. - LookupHandler(get Method, requestPath string) (matchedPattern string, handler HandlerFunc) + // pattern it matched. dstPathVals are set to non-zero values by Mux and can later be accessed by [Exchange.PathValue] + // requestPath is a buffer owned by the [Exchange] usually and should not be held after LookupHandler returns. + LookupHandler(get Method, requestPath []byte, dstPathVals []pathValue) (matchedPattern string, handler HandlerFunc) } // MuxSlice is a [Mux] backed by a slice of registered endpoints, matched by @@ -107,12 +109,102 @@ type Mux interface { type MuxSlice struct { // TODO: binary search worth it? _handlers []struct { - method Method - path string - handler HandlerFunc + method Method + path string + handler HandlerFunc + setPathVal bool } } +type pathValue struct { + Key string // owned by mux. + Value []byte // points to raw exchange buffer. +} + +// pathSeparator is shared so [SetPathValues] never converts a literal per call. +var pathSeparator = []byte{'/'} + +// SetPathValues matches requestPath against pattern and binds its wildcards +// into dstPathVals, read back with [Exchange.PathValue]. Wildcards are whole +// segments as per http.ServeMux: "{name}" takes one non-empty segment, +// "{name...}" the rest including slashes, "{$}" only the path's end, and a +// trailing slash is an anonymous "{...}". i.e: "/b/{bucket}/o/{obj...}". +// +// Unlike ServeMux, segments are compared and bound raw, so "/users/{id}" binds +// "x%2Fy" and not "x/y". Which paths match is unaffected. Bound values alias +// requestPath rather than copy it. +func SetPathValues(dstPathVals []pathValue, pattern string, requestPath []byte) (matched, pathValSliceTooShort bool) { + if len(pattern) == 0 || pattern[0] != '/' || len(requestPath) == 0 || requestPath[0] != '/' { + return false, false + } + pattern, requestPath = pattern[1:], requestPath[1:] + n := 0 + for { + if len(pattern) == 0 { + // Nothing left after a slash: an anonymous "..." taking the rest, + // which is why "/files/" matches "/files/a/b" and "/" matches all. + return true, false + } + patSeg, patRest, patMore := strings.Cut(pattern, "/") + reqSeg, reqRest, reqMore := bytes.Cut(requestPath, pathSeparator) + name, isMulti, isWildcard := pathWildcard(patSeg) + switch { + case isWildcard && name == "$": + // Matches the end of the path and nothing else, so it must be the + // last segment of the pattern and leave no path behind. + return !patMore && len(requestPath) == 0, false + + case isWildcard && isMulti: + // Takes the remainder including slashes, possibly empty. + if name != "" { + if n >= len(dstPathVals) { + return false, true + } + dstPathVals[n] = pathValue{Key: name, Value: requestPath} + n++ + } + return true, false + + case isWildcard: + if len(reqSeg) == 0 { + return false, false // One segment means a non-empty one. + } + if n >= len(dstPathVals) { + return false, true + } + dstPathVals[n] = pathValue{Key: name, Value: reqSeg} + n++ + + default: + if b2s(reqSeg) != patSeg { + return false, false + } + } + if patMore != reqMore { + // One side has a further segment and the other does not, so + // "/health" misses "/health/" and "/files/" misses "/files". + return false, false + } else if !patMore { + return true, false // Both spent on the same segment. + } + pattern, requestPath = patRest, reqRest + } +} + +// pathWildcard picks apart a "{name}" or "{name...}" pattern segment. It +// reports ok false for a literal segment, so "/b_{bucket}" is literal text and +// not a wildcard, matching ServeMux's rule that wildcards be whole segments. +func pathWildcard(segment string) (name string, isMulti, ok bool) { + if len(segment) < 2 || segment[0] != '{' || segment[len(segment)-1] != '}' { + return "", false, false + } + name = segment[1 : len(segment)-1] + if rest, found := strings.CutSuffix(name, "..."); found { + return rest, true, true + } + return name, false, true +} + // Reset discards all registered handlers, reusing the backing array and growing // it to fit capacity registrations. func (sm *MuxSlice) Reset(capacity int) { @@ -121,13 +213,17 @@ func (sm *MuxSlice) Reset(capacity int) { // LookupHandler returns the handler registered for request path, or nil if none matches. // The first registration matching both method and uri wins. -func (sm *MuxSlice) LookupHandler(method Method, path string) (matched string, _ HandlerFunc) { +func (sm *MuxSlice) LookupHandler(method Method, path []byte, dstPathVals []pathValue) (matched string, _ HandlerFunc) { for _, endpoint := range sm._handlers { if endpoint.method != MethUndefined && endpoint.method != method { continue } // Method matches. - if path == endpoint.path { + if endpoint.setPathVal { + if ok, _ := SetPathValues(dstPathVals, endpoint.path, path); ok { + return endpoint.path, endpoint.handler + } + } else if b2s(path) == endpoint.path { return endpoint.path, endpoint.handler } } diff --git a/http/httphi/mux_test.go b/http/httphi/mux_test.go new file mode 100644 index 0000000..23d5135 --- /dev/null +++ b/http/httphi/mux_test.go @@ -0,0 +1,222 @@ +package httphi + +import ( + "strings" + "testing" +) + +// SetPathValues must agree with net/http.ServeMux on which patterns match which +// paths and what each wildcard binds to. Every case below was taken from a run +// against a real ServeMux, so this table is an oracle, not a guess. +// +// The one documented deviation is percent-decoding: ServeMux unescapes segments +// before matching and binding, this does not. See TestSetPathValuesEscaping. +func TestSetPathValues(t *testing.T) { + for _, test := range []struct { + pattern string + path string + match bool + want string // "name=value" pairs joined by "|", in bind order. + }{ + // Single segment wildcard binds exactly one non-empty segment. + {pattern: "/users/{id}", path: "/users/42", match: true, want: "id=42"}, + {pattern: "/users/{id}", path: "/users/42/x", match: false}, + {pattern: "/users/{id}", path: "/users/", match: false}, + {pattern: "/users/{id}", path: "/users", match: false}, + {pattern: "/users/{id}/edit", path: "/users/42/edit", match: true, want: "id=42"}, + {pattern: "/{a}/{b}", path: "/x/y", match: true, want: "a=x|b=y"}, + + // "..." swallows the remainder, slashes included, and may bind empty. + {pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o/a/b/c", match: true, want: "bucket=bk|obj=a/b/c"}, + {pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o/", match: true, want: "bucket=bk|obj="}, + {pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o", match: false}, + {pattern: "/files/{p...}", path: "/files/", match: true, want: "p="}, + {pattern: "/files/{p...}", path: "/files", match: false}, + + // {$} matches only the end of the path. + {pattern: "/{$}", path: "/", match: true}, + {pattern: "/{$}", path: "/x", match: false}, + {pattern: "/a/{$}", path: "/a/", match: true}, + {pattern: "/a/{$}", path: "/a", match: false}, + {pattern: "/a/{$}", path: "/a/b", match: false}, + + // A trailing slash is an anonymous "..." wildcard, binding nothing. + {pattern: "/files/", path: "/files/a/b", match: true}, + {pattern: "/files/", path: "/files/", match: true}, + {pattern: "/files/", path: "/files", match: false}, + {pattern: "/", path: "/anything/at/all", match: true}, + + // Literal patterns match exactly, trailing slash included. + {pattern: "/health", path: "/health", match: true}, + {pattern: "/health", path: "/health/", match: false}, + + // An empty segment never satisfies a single wildcard. + {pattern: "/a/{x}/b", path: "/a//b", match: false}, + } { + t.Run(test.pattern+"__"+test.path, func(t *testing.T) { + vals := make([]pathValue, 8) + match, tooShort := SetPathValues(vals, test.pattern, []byte(test.path)) + if tooShort { + t.Fatal("8 slots must be enough for these patterns") + } + if match != test.match { + t.Fatalf("want match=%v, got %v", test.match, match) + } + if !match { + return + } + if got := renderPathValues(vals); got != test.want { + t.Errorf("want %q, got %q", test.want, got) + } + }) + } +} + +// Bound values must alias the request path buffer rather than copy it: the +// exchange owns that memory and a copy would allocate per request. +func TestSetPathValuesAliasesRequestBuffer(t *testing.T) { + path := []byte("/users/42/edit") + vals := make([]pathValue, 4) + match, _ := SetPathValues(vals, "/users/{id}/edit", path) + if !match { + t.Fatal("want match") + } + if string(vals[0].Value) != "42" { + t.Fatalf("want id=42, got %q", vals[0].Value) + } + // Mutating the request buffer must show through the bound value. + path[7] = '9' + if string(vals[0].Value) != "92" { + t.Errorf("value must alias the request buffer, got %q", vals[0].Value) + } +} + +// A destination too small to hold every wildcard must say so rather than bind a +// partial set or write out of range. +func TestSetPathValuesSliceTooShort(t *testing.T) { + match, tooShort := SetPathValues(make([]pathValue, 1), "/{a}/{b}", []byte("/x/y")) + if !tooShort { + t.Error("want pathValSliceTooShort for 2 wildcards in 1 slot") + } + if match { + t.Error("want match=false when values could not be bound") + } + // A pattern that binds nothing needs no slots at all. + match, tooShort = SetPathValues(nil, "/health", []byte("/health")) + if !match || tooShort { + t.Errorf("want match with no slots needed, got match=%v tooShort=%v", match, tooShort) + } +} + +// Percent escapes are compared and bound raw. ServeMux unescapes segment by +// segment, so "/users/x%2Fy" binds id="x/y" there and id="x%2Fy" here. Matching +// agrees either way; only the bound bytes differ. +func TestSetPathValuesEscaping(t *testing.T) { + vals := make([]pathValue, 4) + if match, _ := SetPathValues(vals, "/a%2Fb/{x}", []byte("/a%2Fb/v")); !match { + t.Error("want literal escape in pattern to match the same bytes in path") + } + vals = make([]pathValue, 4) + match, _ := SetPathValues(vals, "/users/{id}", []byte("/users/x%2Fy")) + if !match { + t.Fatal("want match") + } + if got := string(vals[0].Value); got != "x%2Fy" { + t.Errorf("want raw %q, got %q", "x%2Fy", got) + } +} + +// renderPathValues joins the bound pairs for comparison, stopping at the first +// unused slot. +func renderPathValues(vals []pathValue) string { + var sb strings.Builder + for _, v := range vals { + if v.Key == "" { + break + } + if sb.Len() > 0 { + sb.WriteByte('|') + } + sb.WriteString(v.Key) + sb.WriteByte('=') + sb.Write(v.Value) + } + return sb.String() +} + +// Matching a request must not allocate: keys alias the mux's pattern and values +// alias the request buffer, so nothing is copied per request. +func TestSetPathValuesNoAlloc(t *testing.T) { + vals := make([]pathValue, 8) + path := []byte("/b/bk/o/a/b/c") + allocs := testing.AllocsPerRun(100, func() { + SetPathValues(vals, "/b/{bucket}/o/{obj...}", path) + }) + if allocs != 0 { + t.Fatalf("SetPathValues allocated %v times, want 0", allocs) + } +} + +// pathValueMux binds one wildcard pattern, standing in for a [Mux] that +// supports them until MuxSlice sets setPathVal. +type pathValueMux struct { + pattern string + handler HandlerFunc +} + +func (m *pathValueMux) LookupHandler(method Method, path []byte, dst []pathValue) (string, HandlerFunc) { + if ok, _ := SetPathValues(dst, m.pattern, path); ok { + return m.pattern, m.handler + } + return "", nil +} + +// A wildcard bound by one request must not be readable by the next request the +// same pooled exchange serves. A literal pattern binds nothing, so it never +// overwrites the previous request's slots, and the values it would leak alias a +// buffer the new request has already overwritten. +func TestExchangePathValueClearedBetweenRequests(t *testing.T) { + exch := new(Exchange) + exch.Configure(ExchangeConfig{ + RawBuf: make([]byte, 2048), RequestBufferLim: 1024, + NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: 4, + }) + + // First request binds id=42 off a wildcard pattern. + var gotFirst string + wildcard := &pathValueMux{pattern: "/users/{id}", handler: func(e *Exchange) { + gotFirst = string(e.PathValue("id")) + e.WriteHeader(200) + }} + conn := newConn("GET /users/42 HTTP/1.1\r\nHost: h\r\n\r\n") + conn.Hangup() + if !exch.Acquire(conn) { + t.Fatal("fresh exchange failed to acquire") + } + if err := Handle(exch, wildcard, nopBackoff); err != nil { + t.Fatalf("first request: %s", err) + } + exch.Release() + if gotFirst != "42" { + t.Fatalf("want id=42 bound on the first request, got %q", gotFirst) + } + + // Second request matches a literal pattern, which binds nothing at all. + var leaked []byte + var sm MuxSlice + sm.Handle("/health", func(e *Exchange) { + leaked = e.PathValue("id") + e.WriteHeader(200) + }) + conn2 := newConn("GET /health HTTP/1.1\r\nHost: h\r\n\r\n") + conn2.Hangup() + if !exch.Acquire(conn2) { + t.Fatal("released exchange failed to re-acquire") + } + if err := Handle(exch, &sm, nopBackoff); err != nil { + t.Fatalf("second request: %s", err) + } + if leaked != nil { + t.Errorf("want no path value on a literal route, got id=%q from the previous request", leaked) + } +} diff --git a/http/httphi/router.go b/http/httphi/router.go index 4de5d9e..588be2a 100644 --- a/http/httphi/router.go +++ b/http/httphi/router.go @@ -52,6 +52,7 @@ type Router struct { reqBuf int respBuf int reqNumHeaderCap int + maxPathValues int normalizeKeys bool pendingConns chan job mux Mux @@ -83,6 +84,8 @@ type RouterConfig struct { ResponseHeaderMinBufferSize int // Number of request header key/value pairs to parse before failing and returning [StatusRequestHeaderFieldsTooLarge]. RequestNumHeaderKVCap int + // Sets maximum number of PathValue pairs that can be set on an exchange. Accessed via [Exchange.PathValue]. + MaxPathValues int // NormalizeOutgoingKeys normalizes response header field keys as they are // staged, i.e: "content-type" becomes "Content-Type". @@ -180,11 +183,13 @@ func (r *Router) Configure(cfg RouterConfig) error { gen := r.gen.Load() numgoro := cfg.FixedNumGoroutines workerMode := cfg.workerMode() + r.reqNumHeaderCap = cfg.RequestNumHeaderKVCap r.reqBuf = cfg.RequestHeaderBufferSize r.respBuf = cfg.ResponseHeaderMinBufferSize r.mux = cfg.Mux r.log = cfg.Logger + r.maxPathValues = cfg.MaxPathValues r.normalizeKeys = cfg.NormalizeOutgoingKeys // Freelist entries were sized by the outgoing configuration: recycling one // would serve a request with buffer limits cfg never asked for. @@ -218,6 +223,7 @@ func (r *Router) Configure(cfg RouterConfig) error { NumHeaderKVCap: cfg.RequestNumHeaderKVCap, NormalizeOutgoingKeys: cfg.NormalizeOutgoingKeys, NoRequestBufferGrowth: true, // Hard memory limit. + MaxPathValues: cfg.MaxPathValues, }) go r.goroWorker(gen, jobqueue, cfg.Mux) } @@ -383,6 +389,7 @@ func (r *Router) getExchLocked(conn conn) (exch *Exchange) { NumHeaderKVCap: r.reqNumHeaderCap, NormalizeOutgoingKeys: r.normalizeKeys, NoRequestBufferGrowth: true, + MaxPathValues: r.maxPathValues, }) exch.Acquire(conn) // Fresh exchange, CAS cannot fail. return exch diff --git a/http/httpraw/form.go b/http/httpraw/form.go index 8c509c1..35d39a4 100644 --- a/http/httpraw/form.go +++ b/http/httpraw/form.go @@ -22,6 +22,9 @@ func (f *Form) Reset(buf []byte, capKV int) { // ParseBytes copies the argument bytes to the Form's underlying buffer and parses them. func (f *Form) ParseBytes(b []byte) error { f.Reset(nil, 0) + if len(b) == 0 { + return nil // An empty body is an empty form, not a failure to read one. + } err := f.kv.ReadFromBytes(b) if err != nil { return err @@ -54,7 +57,7 @@ func (f *Form) Decode() error { return err } else if len(v) == 0 { if nk != len(k) { - f.kv.setAt(i, k, v) + f.kv.setAt(i, k[:nk], v) // k[:nk]: the decoded key is shorter. } continue } diff --git a/http/httpraw/form_test.go b/http/httpraw/form_test.go index 1ba0434..7545118 100644 --- a/http/httpraw/form_test.go +++ b/http/httpraw/form_test.go @@ -66,6 +66,29 @@ func TestFormDecode(t *testing.T) { } } +// Decoding a valueless key that shrinks must rewrite the key without inventing +// a value: the pair has no '=' before Decode and must have none after. +func TestFormDecodeValuelessKeyShrinks(t *testing.T) { + var f Form + const body = "a=1&o%6Bay" + if err := f.ParseBytes([]byte(body)); err != nil { + t.Fatal(err) + } + if err := f.Decode(); err != nil { + t.Fatal(err) + } + const want = "a=1|okay" + if got := render(&f); got != want { + t.Errorf("want %q, got %q", want, got) + } + if _, value := f.Pair(1); value != nil { + t.Errorf("want valueless pair to stay valueless, got value %q", value) + } + if !f.Has("okay") { + t.Error("want decoded valueless key present") + } +} + // A malformed escape must be reported, never silently passed through. func TestFormDecodeMalformed(t *testing.T) { for _, body := range []string{"q=%zz", "%zz=v", "q=%4"} { diff --git a/http/httpraw/header.go b/http/httpraw/header.go index 4867d9a..c145d52 100644 --- a/http/httpraw/header.go +++ b/http/httpraw/header.go @@ -241,49 +241,11 @@ func (h *Header) SetInt(key string, value int64, base int) { // Calling Set mangles the buffer. func (h *Header) Set(key, value string) (enoughSpace bool) { return h.hbuf.kv.Set(key, value) - - // useKv := h.takeReusableSlot(key) - // if useKv == nil { - // h.hbuf.kv.appendPair(key, value) - // } else { - // useKv.value = h.hbuf.kv.reuseOrAppend(useKv.value, value) - // } -} - -// takeReusableSlot returns the valid key-value entry for key with the largest -// value buffer (best candidate for in-place reuse) and invalidates any other -// entries sharing the key. Returns nil if the key is not present. -func (h *Header) takeReusableSlot(key string) *argsKV { - // hb := &h.hbuf - var useKv *argsKV - // for i := 0; i < len(hb.headers); i++ { - // // Search for key-value with largest buffer for value to store value reusing buffer. - // gotkv := &hb.headers[i] - // if gotkv.isValidHeader() && b2s(hb.musttoken(gotkv.key)) == key { - // if useKv == nil { - // useKv = gotkv - // } else if gotkv.value.len > useKv.value.len { - // useKv.invalidate() - // useKv = gotkv - // } else { - // gotkv.invalidate() - // } - // } - // } - return useKv } // Get gets the first exact-match value of a key found in the headers. Use [Header.ForEach] to find multiple values corresponding to same key. func (h *Header) Get(key string) []byte { return h.hbuf.kv.Get(key) - // debuglog("http:get:start") - // kv := h.peekHeader(key) - // if kv.isValidHeader() { - // debuglog("http:get:found") - // return h.hbuf.musttoken(kv.value) - // } - // debuglog("http:get:notfound") - // return nil } // GetFold gets the first value whose key matches key under ASCII case-insensitive diff --git a/http/httpraw/kvbuffer.go b/http/httpraw/kvbuffer.go index 22784d1..2986d9e 100644 --- a/http/httpraw/kvbuffer.go +++ b/http/httpraw/kvbuffer.go @@ -225,9 +225,11 @@ func (mb *KVBuffer) At(i int) (key, value []byte) { } func (mb *KVBuffer) setAt(i int, k, v []byte) { mb.flags |= flagMangledBuffer + // Route through slice, not bytes2tok: a nil v is a pair with no '=' and must + // stay absent rather than trip the alias check on a nil pointer. mb.kvs[i] = argsKV{ - key: bytes2tok(mb.buf, k), - value: bytes2tok(mb.buf, v), + key: mb.slice(k), + value: mb.slice(v), } } diff --git a/http/httpraw/parse.go b/http/httpraw/parse.go index a025deb..07d9e0a 100644 --- a/http/httpraw/parse.go +++ b/http/httpraw/parse.go @@ -12,10 +12,10 @@ var ( errNoProto = errors.New("missing protocol, HTTP/0.9 unsupported") // ErrNeedMoreData signals a parser was handed an incomplete buffer: append // more data to it and call again. - ErrNeedMoreData = errors.New("need more data: cannot find trailing lf/delimiter") - errNoBoundary = errors.New("httpraw: multipart boundary not set") - errUnparsed = errors.New("need to finish parsing") - errInvalidName = errors.New("invalid header name") + ErrNeedMoreData = errors.New("need more data: cannot find trailing lf/delimiter") + errNoBoundary = errors.New("httpraw: multipart boundary not set") + errUnparsed = errors.New("need to finish parsing") + errInvalidName = errors.New("invalid header name") // ErrBufferExhausted signals a buffer with no room left for the data being // written and no permission to grow, see [KVBuffer.EnableBufferGrowth]. // Enlarging the buffer handed to Reset is the only fix; a server answers it