Further improvements to httphi API (#173)

* add Exchange.WriteBodyString

* Mux.MaxPathValues and other improvements

* Mux PathValue improvemnt and fixes

* MuxSlice more method muxing improvements

* diagram out interesting approach to form parsing for clanker

* refactor RequestParseForm and achieve greatness in API design

* explicit naming of headerCapacityKV value in kvBuffer.Reset

* fix Mux bug not matching paths correctly; httpraw HTTP V1 naming applied

* rename many examples,use httphi in examples,remove useless maxAwaitingConn field

* add ipv4.String

* add ipv4 UnspecifiedAddr and BroadcastAddr

* add ethernet.String
This commit is contained in:
Pat Whittingslow
2026-07-31 12:47:33 -03:00
committed by GitHub
parent 5c54030f19
commit 1884cfc9b7
35 changed files with 2192 additions and 406 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"bytes"
)
// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [Header] methods.
// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [HeaderV1] methods.
// Cookie represents a single-line Cookie header value in a HTTP header, much like the standard library Cookie.
type Cookie struct {
kv kvBuffer
@@ -16,7 +16,7 @@ func (c *Cookie) EnableBufferGrowth(enableBufferGrowth bool) {
c.kv.EnableBufferGrowth(enableBufferGrowth)
}
// Reset functions very similarly to [Header.Reset]. Can be used for in-place cookie parsing.
// Reset functions very similarly to [HeaderV1.Reset]. Can be used for in-place cookie parsing.
func (c *Cookie) Reset(buf []byte, capKV int) { c.kv.Reset(buf, capKV) }
func (c *Cookie) valid() bool {
+18 -1
View File
@@ -1,6 +1,9 @@
package httpraw
import "bytes"
import (
"bytes"
"io"
)
// Form holds "application/x-www-form-urlencoded" key-value pairs, the encoding
// HTML forms use for POST bodies and query strings alike. Methods function
@@ -19,10 +22,24 @@ func (f *Form) EnableBufferGrowth(enableGrowth bool) { f.kv.EnableBufferGrowth(e
// Reset discards parsed pairs and sets the buffer to parse in place.
// If buf is nil the current buffer is reused.
//
// capKV sizes the pair table. With growth disabled it is a hard limit, so a
// capKV of 0 leaves no room for a single pair and [Form.Parse] answers
// [ErrBufferExhausted]; size it to the pairs expected.
func (f *Form) Reset(buf []byte, capKV int) {
f.kv.Reset(buf, capKV)
}
// ReadFromBytes appends buf to the underlying buffer, accumulating data to parse. Returns ErrBufferExhausted when buf does not fit and growth is disabled.
func (f *Form) ReadFromBytes(b []byte) error { return f.kv.ReadFromBytes(b) }
// BufferUsed returns bytes accumulated by the Read* methods and awaiting a
// [Form.Parse]. See [kvBuffer.BufferUsed].
func (f *Form) BufferUsed() int { return f.kv.BufferUsed() }
// ReadLimited appends at most limit bytes read from r to the underlying buffer. A read returning data alongside io.EOF reports a nil error, later ones io.EOF.
func (f *Form) ReadLimited(r io.Reader, limit int) (int, error) { return f.kv.ReadLimited(r, limit) }
// 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)
+42 -1
View File
@@ -148,10 +148,51 @@ func TestFormParseReuseNoAlloc(t *testing.T) {
t.Fatal(err)
}
allocs := testing.AllocsPerRun(100, func() {
f.Reset(body, 0)
f.Reset(body, 0) // 0 preserves the pair storage warmed up above, the reuse under test.
f.Parse()
})
if allocs != 0 {
t.Errorf("reused Form allocated %v times, want 0", allocs)
}
}
// BufferUsed reports buffered bytes, not parsed pairs, so a caller appending
// from several sources can tell whether a separator is needed before the next
// one. Form.Len is zero until Parse runs and cannot answer that.
func TestFormBufferUsed(t *testing.T) {
var f Form
f.Reset(nil, defaultKVCap)
if got := f.BufferUsed(); got != 0 {
t.Errorf("want 0 on a fresh form, got %d", got)
}
if err := f.ReadFromBytes([]byte("a=1")); err != nil {
t.Fatal(err)
}
if got := f.BufferUsed(); got != 3 {
t.Errorf("want 3 buffered, got %d", got)
}
if got := f.Len(); got != 0 {
t.Errorf("Len must stay 0 until Parse, got %d", got)
}
// A second source appended behind a separator.
if err := f.ReadFromBytes([]byte("&b=2")); err != nil {
t.Fatal(err)
}
if got := f.BufferUsed(); got != 7 {
t.Errorf("want 7 buffered, got %d", got)
}
if err := f.Parse(); err != nil {
t.Fatal(err)
}
if got := render(&f); got != "a=1|b=2" {
t.Errorf("want a=1|b=2, got %q", got)
}
if got := f.BufferUsed(); got != 7 {
t.Errorf("BufferUsed must not change on Parse, got %d", got)
}
// Reset discards the pairs and the buffered bytes with them.
f.Reset(nil, defaultKVCap)
if got := f.BufferUsed(); got != 0 {
t.Errorf("want 0 after Reset, got %d", got)
}
}
@@ -8,7 +8,8 @@ import (
const (
methodGet = "GET"
strHTTP11 = "HTTP/1.1"
strHTTP1 = "HTTP/1."
strHTTP11 = strHTTP1 + "1"
strCRLF = "\r\n"
headerCookie = "Cookie"
headerConnection = "Connection"
@@ -19,7 +20,7 @@ const (
// Flags is a bitset of signals gathered while parsing or building a header,
// such as a status code having been set or the peer requesting connection
// close. See [Header.Flags].
// close. See [HeaderV1.Flags].
type Flags uint16
const (
@@ -40,15 +41,15 @@ func (f Flags) HasAny(checkThese Flags) bool {
return f&checkThese != 0
}
// Header implements "raw" HTTP header key-value parsing, validation and marshalling.
// HeaderV1 implements "raw" HTTP header key-value parsing, validation and marshalling.
//
// It does NOT implement:
// - Normalization.
// - Cookies (see [Cookie]).
// - Special header optimizations.
// - Content-Length validation and other special header field value validation.
type Header struct {
hbuf headerBuf
type HeaderV1 struct {
hbuf headerv1Buf
// Request fields.
method view
@@ -62,18 +63,18 @@ type Header struct {
}
// Flags returns [Flags] to signal status code has been set, Connection:Close or other useful signals provided by flags.
func (h *Header) Flags() Flags { return h.hbuf.kv.flags }
func (h *HeaderV1) Flags() Flags { return h.hbuf.kv.flags }
// ConfigBufferGrowth configures the memory the header may use. Setting
// outlives [Header.Reset]. Call before parsing/reading.
// outlives [HeaderV1.Reset]. Call before parsing/reading.
//
// enableBufferGrowth enables growing both the header buffer and the header key/value pair slice.
func (h *Header) ConfigBufferGrowth(enableBufferGrowth bool) {
func (h *HeaderV1) ConfigBufferGrowth(enableBufferGrowth bool) {
h.hbuf.kv.EnableBufferGrowth(enableBufferGrowth)
}
// ParseBytes copies the bytes into buffer and parses the HTTP header. It fails if HTTP header data is incomplete.
func (h *Header) ParseBytes(asResponse bool, b []byte) error {
func (h *HeaderV1) ParseBytes(asResponse bool, b []byte) error {
h.Reset(nil, 0)
err := h.hbuf.kv.ReadFromBytes(b)
if err != nil {
@@ -82,9 +83,9 @@ func (h *Header) ParseBytes(asResponse bool, b []byte) error {
return h.parse(asResponse)
}
// Parse parses accumulated data in-place with no copying. One can set HTTP header data buffer with [Header.Reset].
// Parse parses accumulated data in-place with no copying. One can set HTTP header data buffer with [HeaderV1.Reset].
// It fails if HTTP data is incomplete.
func (h *Header) Parse(asResponse bool) error {
func (h *HeaderV1) Parse(asResponse bool) error {
debuglog("http:parse:reset")
h.Reset(h.hbuf.kv.buf, 0)
debuglog("http:parse:start")
@@ -93,7 +94,7 @@ func (h *Header) Parse(asResponse bool) error {
// TryParse begins parsing or resumes parsing from a failed previous attempt from any of the Parse* methods.
// As long as needMoreData returns true future calls to TryParse may succeed and the header is not done parsing.
// Users may call [Header.ForEach] in-between TryParse calls so as to validate values before header is completely parsed.
// Users may call [HeaderV1.ForEach] in-between TryParse calls so as to validate values before header is completely parsed.
//
// needMoreData := true
// var err error
@@ -107,7 +108,7 @@ func (h *Header) Parse(asResponse bool) error {
// if err != nil {
// return err
// }
func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) {
func (h *HeaderV1) TryParse(asResponse bool) (needMoreData bool, err error) {
flags := h.Flags()
if flags.HasAny(flagDoneParsingHeader) {
return false, errAlreadyParsed
@@ -125,26 +126,26 @@ func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) {
}
// ParsingSuccess returns true if TryParse was successful, that is to say it returned needMoreData==false and err==nil.
func (h *Header) ParsingSuccess() bool {
func (h *HeaderV1) ParsingSuccess() bool {
return h.Flags().HasAny(flagDoneParsingHeader)
}
// ReadFromLimited reads at most maxBytesToRead from reader and appends them to underlying buffer.
// Used to accumulate HTTP header for later parsing with [Header.TryParse].
// Used to accumulate HTTP header for later parsing with [HeaderV1.TryParse].
// If read is successful (read length>0) and reader returns [io.EOF] then ReadFromLimited will return a nil error.
func (h *Header) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) {
func (h *HeaderV1) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) {
return h.hbuf.kv.ReadLimited(r, maxBytesToRead)
}
// ReadFromBytes appends argument buffer to underlying buffer.
// Used to accumulate HTTP header for later parsing with [Header.TryParse].
func (h *Header) ReadFromBytes(b []byte) error {
// Used to accumulate HTTP header for later parsing with [HeaderV1.TryParse].
func (h *HeaderV1) ReadFromBytes(b []byte) error {
return h.hbuf.kv.ReadFromBytes(b)
}
// BufferReceived returns the amoung of bytes read during calls to Read* methods.
// Returns 0 if buffer is invalid/mangled.
func (h *Header) BufferReceived() int {
func (h *HeaderV1) BufferReceived() int {
if h.Flags().HasAny(flagMangledBuffer | flagOOMReached) {
return 0
}
@@ -154,7 +155,7 @@ func (h *Header) BufferReceived() int {
// BufferParsed returns the amount of bytes parsed during a call to Parse* methods.
// If the Parse* method completed without error then BufferParsed returns the header's length including the final "\r\n\r\n" text.
// BufferParsed returns 0 if the buffer is invalid/mangled or if no header data has been parsed succesfully.
func (h *Header) BufferParsed() int {
func (h *HeaderV1) BufferParsed() int {
if h.Flags().HasAny(flagMangledBuffer | flagOOMReached) {
return 0
}
@@ -162,56 +163,56 @@ func (h *Header) BufferParsed() int {
}
// BufferRaw returns the undeerlying buffer as stored currently in memory.
// The length of the returned buffer is the used portion. Capacity of returned slice is [Header.BufferCapacity].
func (h *Header) BufferRaw() []byte { return h.hbuf.kv.BufferRaw() }
// The length of the returned buffer is the used portion. Capacity of returned slice is [HeaderV1.BufferCapacity].
func (h *HeaderV1) BufferRaw() []byte { return h.hbuf.kv.BufferRaw() }
// BufferUsed returns the raw memory used.
//
// BufferUsed + BufferFree == BufferCapacity
func (h *Header) BufferUsed() int {
func (h *HeaderV1) BufferUsed() int {
return len(h.hbuf.kv.BufferRaw())
}
// BufferFree returns amount of bytes free in underlying buffer.
//
// BufferUsed + BufferFree == BufferCapacity
func (h *Header) BufferFree() int {
func (h *HeaderV1) BufferFree() int {
return h.hbuf.free()
}
// BufferCapacity returns the total capacity of the underlying buffer.
//
// BufferUsed + BufferFree == BufferCapacity
func (h *Header) BufferCapacity() int {
func (h *HeaderV1) BufferCapacity() int {
return cap(h.hbuf.kv.BufferRaw())
}
// ForEach iterates over header key-value field tuples.
func (h *Header) ForEach(cb func(key, value []byte) bool) {
func (h *HeaderV1) ForEach(cb func(key, value []byte) bool) {
h.hbuf.kv.ForEach(cb)
}
// Reset discards all parsed data and sets the buffer data to buf. This method
// can be used to avoid copying and growing buffers. Call [Header.Parse] after setting buffer
// can be used to avoid copying and growing buffers. Call [HeaderV1.Parse] after setting buffer
// data with Reset to parse data in-place.
// If buf is nil then the current buffer is reused. There are 3 ways to use Reset:
//
// h.Reset(prealloc[:0], 16); h.ParseBytes(httpHeader) // Tell header to use a pre-allocated buffer capacity.
// h.Reset(httpHeader, 16); h.Parse() // Parse bytes in place with no copying.
// h.Reset(nil) // Reuse buffer previously set in a call to Reset.
func (h *Header) Reset(buf []byte, numHeaderCapacity int) {
func (h *HeaderV1) Reset(buf []byte, numHeaderCapacity int) {
const persistentFlags = flagNoBufferGrow
debuglog("http:reset:hbuf")
h.hbuf.reset(buf, numHeaderCapacity)
if h.Flags().HasAny(flagNoBufferGrow) && h.BufferCapacity() < 32 {
panic("small buffer and flagNoBufferGrow set")
}
*h = Header{hbuf: h.hbuf}
*h = HeaderV1{hbuf: h.hbuf}
debuglog("http:reset:done")
}
// Body returns the surplus data following headers. It is only valid as long as Parse* or Reset methods are not called.
func (h *Header) Body() ([]byte, error) {
func (h *HeaderV1) Body() ([]byte, error) {
debuglog("http:body")
flags := h.Flags()
if flags.HasAny(flagMangledBuffer) {
@@ -222,16 +223,16 @@ func (h *Header) Body() ([]byte, error) {
return nil, errUnparsed
}
// SetBytes is equivalent to [Header.Set] but with a []byte value. Does not keep reference to value slice.
// SetBytes is equivalent to [HeaderV1.Set] but with a []byte value. Does not keep reference to value slice.
// Calling SetBytes Mangles the buffer.
func (h *Header) SetBytes(key string, value []byte) {
func (h *HeaderV1) SetBytes(key string, value []byte) {
h.Set(key, b2s(value))
}
// SetInt is equivalent to [Header.Set] but with an integer value i.e: Content-Length header key.
// SetInt is equivalent to [HeaderV1.Set] but with an integer value i.e: Content-Length header key.
// base must be in the range 2..36 (as accepted by [strconv.AppendInt]); other bases are dropped.
// SetInt formats the value directly into the header buffer without heap allocation.
func (h *Header) SetInt(key string, value int64, base int) {
func (h *HeaderV1) SetInt(key string, value int64, base int) {
if base < 2 || base > 36 {
return // strconv.AppendInt only supports base 2..36.
}
@@ -240,25 +241,25 @@ func (h *Header) SetInt(key string, value int64, base int) {
// Set sets a key-value pair in the HTTP header.
// Calling Set mangles the buffer.
func (h *Header) Set(key, value string) (enoughSpace bool) {
func (h *HeaderV1) Set(key, value string) (enoughSpace bool) {
return h.hbuf.kv.Set(key, value)
}
// 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 {
// Get gets the first exact-match value of a key found in the headers. Use [HeaderV1.ForEach] to find multiple values corresponding to same key.
func (h *HeaderV1) Get(key string) []byte {
return h.hbuf.kv.Get(key)
}
// GetFold gets the first value whose key matches key under ASCII case-insensitive
// comparison, i.e: "content-length" matches "Content-Length".
// Use [Header.Get] for exact match and [Header.ForEach] to find multiple values
// Use [HeaderV1.Get] for exact match and [HeaderV1.ForEach] to find multiple values
// corresponding to same key.
func (h *Header) GetFold(key string) []byte {
func (h *HeaderV1) GetFold(key string) []byte {
return h.hbuf.kv.GetFold(key)
}
// NormalizeKeys normalizes all header keys. i.e: CONTENT-type -> Content-Type
func (h *Header) NormalizeKeys() {
func (h *HeaderV1) NormalizeKeys() {
for i, kv := range h.hbuf.kv.kvs {
if kv.isValidHeader() {
NormalizeHeaderKey(h.hbuf.kv.AtKey(i))
@@ -268,7 +269,7 @@ func (h *Header) NormalizeKeys() {
// ContentLength returns the body length declared by the Content-Length field.
// If the field is not present then the returned bool is false. Will return error for invalid or non-integer value.
func (h *Header) ContentLength() (_ int64, present bool, _ error) {
func (h *HeaderV1) ContentLength() (_ int64, present bool, _ error) {
value := h.GetFold(headerContentLength)
if value == nil {
return 0, false, nil
@@ -283,34 +284,34 @@ func (h *Header) ContentLength() (_ int64, present bool, _ error) {
}
// Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer.
func (h *Header) Add(key, value string) {
func (h *HeaderV1) Add(key, value string) {
h.hbuf.kv.appendPair(key, value)
}
// Method returns HTTP request method.
func (h *Header) Method() []byte {
func (h *HeaderV1) Method() []byte {
return h.getNonEmptyValue(h.method)
}
// SetMethod sets the request header's method.
func (h *Header) SetMethod(method string) {
func (h *HeaderV1) SetMethod(method string) {
h.method = h.hbuf.kv.reuseOrAppend(h.method, method)
}
// SetRequestTarget sets request-target (URI) for the first HTTP request line.
func (h *Header) SetRequestTarget(requestTarget string) {
func (h *HeaderV1) SetRequestTarget(requestTarget string) {
h.requestTarget = h.hbuf.kv.reuseOrAppend(h.requestTarget, requestTarget)
}
// RequestTarget returns a view of the request-target (URI) of the first HTTP request line.
// Called Request-URI in the obsolete RFC 2616, renamed request-target by RFC 9112.
func (h *Header) RequestTarget() []byte {
func (h *HeaderV1) RequestTarget() []byte {
return h.getNonEmptyValue(h.requestTarget)
}
// RequestPath returns the request-target (URI) up to the query string, i.e: "/search"
// for "/search?q=go". Returns the whole target if it contains no query string.
func (h *Header) RequestPath() []byte {
func (h *HeaderV1) RequestPath() []byte {
target := h.RequestTarget()
before, _, ok := bytes.Cut(target, []byte{'?'})
if !ok {
@@ -322,7 +323,7 @@ func (h *Header) RequestPath() []byte {
// RequestQuery returns the request-target (URI) query string as it appears on the
// wire, percent-encoded and with '+' undecoded, i.e: "q=go" for "/search?q=go".
// Returns nil if the target has no query string. Iterate it with [NextQueryPair].
func (h *Header) RequestQuery() []byte {
func (h *HeaderV1) RequestQuery() []byte {
target := h.RequestTarget()
_, after, ok := bytes.Cut(target, []byte{'?'})
if !ok {
@@ -332,17 +333,17 @@ func (h *Header) RequestQuery() []byte {
}
// Protocol returns the request header's HTTP protocol. Usually "HTTP/1.1".
func (h *Header) Protocol() []byte {
func (h *HeaderV1) Protocol() []byte {
return h.getNonEmptyValue(h.proto)
}
// SetProtocol sets the request header's protocol. Usually "HTTP/1.1".
func (h *Header) SetProtocol(protocol string) {
func (h *HeaderV1) SetProtocol(protocol string) {
h.proto = h.hbuf.kv.reuseOrAppend(h.proto, protocol)
}
// Status returns the response header's status code and status text. i.e: "200" "OK".
func (h *Header) Status() (code, statusText []byte) {
func (h *HeaderV1) Status() (code, statusText []byte) {
if h.statusCode.len == 0 {
return nil, nil
}
@@ -350,20 +351,20 @@ func (h *Header) Status() (code, statusText []byte) {
}
// SetStatus sets the response header's status code and status text. i.e: "200" "OK".
func (h *Header) SetStatus(code, statusText string) {
func (h *HeaderV1) SetStatus(code, statusText string) {
h.hbuf.kv.flags |= FlagStatusSet
h.statusCode = h.hbuf.kv.reuseOrAppend(h.statusCode, code)
h.statusText = h.hbuf.kv.reuseOrAppend(h.statusText, statusText)
}
// SetStatusInt is identical to [Header.SetStatus] but performs integer to text conversion for status code.
func (h *Header) SetStatusInt(code int64, statusText string) {
// SetStatusInt is identical to [HeaderV1.SetStatus] but performs integer to text conversion for status code.
func (h *HeaderV1) SetStatusInt(code int64, statusText string) {
h.hbuf.kv.flags |= FlagStatusSet
h.statusCode = h.hbuf.kv.reuseOrAppendInt(h.statusCode, code, 10)
h.statusText = h.hbuf.kv.reuseOrAppend(h.statusText, statusText)
}
func (h *Header) getNonEmptyValue(s view) []byte {
func (h *HeaderV1) getNonEmptyValue(s view) []byte {
if s.len == 0 {
return nil // If empty then value is invalid, return nil.
}
@@ -371,7 +372,7 @@ func (h *Header) getNonEmptyValue(s view) []byte {
}
// AppendRequest appends the request header representation to the buffer and returns the result.
func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
func (h *HeaderV1) AppendRequest(dst []byte) ([]byte, error) {
proto := h.Protocol()
if h.hbuf.kv.flags.HasAny(flagOOMReached) {
return dst, ErrBufferExhausted
@@ -401,7 +402,7 @@ func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
}
// AppendResponse appends the response header representation to the buffer and returns the result.
func (h *Header) AppendResponse(dst []byte) ([]byte, error) {
func (h *HeaderV1) AppendResponse(dst []byte) ([]byte, error) {
dst, err := h.AppendResponseNoHeaders(dst)
if err != nil {
return dst, err
@@ -411,7 +412,7 @@ func (h *Header) AppendResponse(dst []byte) ([]byte, error) {
}
// AppendResponseNoHeaders appends the first line of the response containing protocol and status code/text: i.e: "HTTP/1.1 200 OK\r\n"
func (h *Header) AppendResponseNoHeaders(dst []byte) ([]byte, error) {
func (h *HeaderV1) AppendResponseNoHeaders(dst []byte) ([]byte, error) {
proto := h.Protocol()
if h.hbuf.kv.flags.HasAny(flagOOMReached) {
return dst, ErrBufferExhausted
@@ -433,7 +434,7 @@ func (h *Header) AppendResponseNoHeaders(dst []byte) ([]byte, error) {
// AppendHeaders appends headers to buffer. Use AppendRequest and AppendResponse over this.
// Does not append extra \r\n to end. Appends nothing if contains no headers.
func (h *Header) AppendHeaders(dst []byte) []byte {
func (h *HeaderV1) AppendHeaders(dst []byte) []byte {
for i, kv := range h.hbuf.kv.kvs {
if kv.isValidHeader() {
k, v := h.hbuf.kv.At(i)
@@ -446,7 +447,7 @@ func (h *Header) AppendHeaders(dst []byte) []byte {
// String returns the header's wire representation, as a request if it has a
// request line and as a response otherwise. Returns the error text if neither
// can be built. Allocates, so it is meant for debugging and logging only.
func (h *Header) String() string {
func (h *HeaderV1) String() string {
buf, err := h.AppendRequest(nil)
if err != nil {
buf, err = h.AppendResponse(nil)
@@ -11,7 +11,11 @@ import (
"time"
)
const numHeaderCapacity = 16
// defaultKVCap is the key/value table size tests hand to Reset. A Reset with 0
// preserves whatever capacity the value already had, which is what production
// code wants on reuse but leaves a fresh value unable to hold a single pair when
// growth is disabled, see [Form.Reset].
const defaultKVCap = 16
func TestHeaderParseRequest(t *testing.T) {
const (
@@ -40,7 +44,7 @@ func TestHeaderParseRequest(t *testing.T) {
var buf bytes.Buffer
req.Write(&buf)
var hdr Header
var hdr HeaderV1
msg := buf.Bytes()
start := time.Now()
@@ -62,7 +66,7 @@ func TestHeaderParseRequest(t *testing.T) {
}
var c Cookie
cookie := hdr.Get("Cookie")
c.Reset(cookie, 0)
c.Reset(cookie, defaultKVCap)
err = c.Parse()
if err != nil {
t.Error(err)
@@ -124,7 +128,7 @@ func BenchmarkParseBytes(b *testing.B) {
// allocating on every iteration. Declaring it inside the loop causes
// two allocs per iteration: one for the headers slice (make in reset)
// and one for the data buffer (append in readFromBytes).
var hdr Header
var hdr HeaderV1
b.StartTimer()
for b.Loop() {
@@ -165,7 +169,7 @@ func TestHeaderRequestPath(t *testing.T) {
{uri: "/a/b/c?x=1&y=2", want: "/a/b/c"},
{uri: "/?q=go", want: "/"},
} {
var h Header
var h HeaderV1
err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n"))
if err != nil {
t.Fatal(err)
@@ -192,7 +196,7 @@ func TestHeaderContentLength(t *testing.T) {
{field: "Content-Length: 1 2", wantErr: errBadContentLength}, // Not a list.
{field: "Content-Length: 9223372036854775808", wantErr: errBadContentLength},
} {
var h Header
var h HeaderV1
raw := "POST / HTTP/1.1\r\nHost: h\r\n"
if test.field != "" {
raw += test.field + "\r\n"
@@ -230,7 +234,7 @@ func TestNextQueryPair(t *testing.T) {
{uri: "/x?a%20b=c%20d", want: "a%20b=c%20d"}, // Raw, undecoded.
{uri: "/x?a=b=c", want: "a=b=c"}, // Only first '=' splits.
} {
var h Header
var h HeaderV1
err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n"))
if err != nil {
t.Fatal(err)
@@ -393,8 +397,8 @@ func TestCopyDecodedPercentURLInPlace(t *testing.T) {
}
func TestHeaderSetOverwrite(t *testing.T) {
var h Header
h.Reset(nil, numHeaderCapacity)
var h HeaderV1
h.Reset(nil, defaultKVCap)
h.SetMethod("GET")
h.SetRequestTarget("/")
h.SetProtocol("HTTP/1.1")
@@ -416,8 +420,8 @@ func TestHeaderSetOverwrite(t *testing.T) {
}
func TestHeaderSetBytesEmptyValue(t *testing.T) {
var h Header
h.Reset(nil, numHeaderCapacity)
var h HeaderV1
h.Reset(nil, defaultKVCap)
h.SetBytes("X-Empty", nil)
if got := h.Get("X-Empty"); len(got) != 0 {
t.Errorf("want empty value, got %q", got)
@@ -436,7 +440,7 @@ func TestHeader_LargeBufferOverflow(t *testing.T) {
"X-Canary: " + wantVal + "\r\n" +
"\r\n"
var h Header
var h HeaderV1
err := h.ParseBytes(false, []byte(raw))
if err != nil {
// Clean rejection of the oversized header is the intended behavior:
@@ -454,7 +458,7 @@ func TestHeader_LargeBufferOverflow(t *testing.T) {
// not ErrNeedMoreData (which makes a streaming parser wait forever).
func TestHeader_ColonlessLineIsHardError(t *testing.T) {
raw := "GET / HTTP/1.1\r\nBadHeaderNoColon\r\n\r\n"
var h Header
var h HeaderV1
err := h.ParseBytes(false, []byte(raw))
if err == nil {
t.Fatal("want error on colonless header line, got nil")
@@ -471,8 +475,8 @@ func TestHeader_SplitBeforeColonStillParses(t *testing.T) {
const part1 = "GET / HTTP/1.1\r\nHost" // split mid-key, before colon+newline
const part2 = ": example.com\r\n\r\n"
var h Header
h.Reset(nil, numHeaderCapacity)
var h HeaderV1
h.Reset(nil, defaultKVCap)
if err := h.ReadFromBytes([]byte(part1)); err != nil {
t.Fatal(err)
}
@@ -504,8 +508,8 @@ func TestHeader_SplitBeforeColonStillParses(t *testing.T) {
func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) {
const key, value = "K", "V"
buf := make([]byte, 0, len(key)+len(value)) // exact cap, no slack.
var h Header
h.Reset(buf, numHeaderCapacity)
var h HeaderV1
h.Reset(buf, defaultKVCap)
defer func() {
if r := recover(); r != nil {
t.Fatalf("appendHeader panicked on exact-cap buffer: %v", r)
@@ -521,8 +525,8 @@ func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) {
// never panic. Panicking is unacceptable for this package.
func TestHeader_AddFullBufferNoPanic(t *testing.T) {
buf := make([]byte, 0, 40) // Small cap; enough for Reset (len 0) but not the field below.
var h Header
h.Reset(buf, numHeaderCapacity)
var h HeaderV1
h.Reset(buf, defaultKVCap)
h.ConfigBufferGrowth(false)
h.SetMethod("GET")
h.SetRequestTarget("/")
@@ -556,8 +560,8 @@ func TestHeader_SetInt(t *testing.T) {
{"hex", 255, 16, "ff"},
} {
t.Run(tc.name, func(t *testing.T) {
var h Header
h.Reset(nil, numHeaderCapacity)
var h HeaderV1
h.Reset(nil, defaultKVCap)
h.SetInt("Content-Length", tc.value, tc.base)
if got := string(h.Get("Content-Length")); got != tc.want {
t.Fatalf("want %q, got %q", tc.want, got)
@@ -568,8 +572,8 @@ func TestHeader_SetInt(t *testing.T) {
// SetInt on an existing key must reuse the slot in place (single field, latest value).
func TestHeader_SetIntOverwrite(t *testing.T) {
var h Header
h.Reset(nil, numHeaderCapacity)
var h HeaderV1
h.Reset(nil, defaultKVCap)
h.SetMethod("GET")
h.SetRequestTarget("/")
h.SetProtocol("HTTP/1.1")
@@ -592,8 +596,8 @@ func TestHeader_SetIntOverwrite(t *testing.T) {
// SetInt must not heap-allocate: it must format directly into the header buffer.
func TestHeader_SetIntNoAlloc(t *testing.T) {
buf := make([]byte, 0, 256)
var h Header
h.Reset(buf, numHeaderCapacity)
var h HeaderV1
h.Reset(buf, defaultKVCap)
h.ConfigBufferGrowth(false)
h.Add("Content-Length", "0000000000000000000000") // pre-size a reusable slot.
allocs := testing.AllocsPerRun(100, func() {
@@ -622,8 +626,8 @@ func TestHeader_FieldTableSizedFromBuffer(t *testing.T) {
}
raw.WriteString("X-Canary: " + wantVal + "\r\n\r\n")
var h Header
h.Reset(make([]byte, 0, 8192), numHeaderCapacity) // Room for the block with plenty to spare.
var h HeaderV1
h.Reset(make([]byte, 0, 8192), defaultKVCap) // Room for the block with plenty to spare.
err := h.ParseBytes(false, []byte(raw.String()))
if err != nil {
t.Fatalf("parsing a 42 field request into an 8kB buffer: %s", err)
@@ -645,8 +649,8 @@ func TestHeader_FieldTableFullIsReported(t *testing.T) {
raw.WriteString(":v\r\n")
}
raw.WriteString("\r\n")
var h Header
h.Reset(make([]byte, 0, 512), numHeaderCapacity)
var h HeaderV1
h.Reset(make([]byte, 0, 512), defaultKVCap)
h.ConfigBufferGrowth(false)
err := h.ParseBytes(false, []byte(raw.String()))
if !errors.Is(err, ErrHeaderTooMany) {
+9 -4
View File
@@ -23,6 +23,11 @@ func (kvb *kvBuffer) free() int { return cap(kvb.buf) - len(kvb.buf) }
// Stored pairs alias it, so writing to it mangles them.
func (kvb *kvBuffer) BufferRaw() []byte { return kvb.buf }
// BufferUsed returns the raw memory used, which is what a caller appending from
// several sources checks to know whether a separator is needed. Counts buffered
// bytes and not parsed pairs, so it is set before a Parse and unchanged by one.
func (kvb *kvBuffer) BufferUsed() int { return len(kvb.buf) }
// EnableBufferGrowth allows the buffer to grow past the memory [kvBuffer.Reset]
// was handed. The setting outlives Reset; with growth off callers get [ErrBufferExhausted].
func (kvb *kvBuffer) EnableBufferGrowth(enableGrowth bool) {
@@ -285,17 +290,17 @@ func (kvb *kvBuffer) getIdx(key string) int {
func (kvb *kvBuffer) getFoldIdx(key string) int {
for i, pair := range kvb.kvs {
if pair.isValid() && asciiEqualFold(key, b2s(kvb.AtKey(i))) {
if pair.isValid() && EqualFoldASCII(key, b2s(kvb.AtKey(i))) {
return i
}
}
return -1
}
// asciiEqualFold reports whether a and b are equal under ASCII case folding.
// EqualFoldASCII reports whether a and b are equal under ASCII case folding.
// Unlike strings.EqualFold it does not fold non-ASCII runes, so no multi-byte
// rune such as U+212A KELVIN SIGN can alias a header key.
func asciiEqualFold(a, b string) bool {
func EqualFoldASCII(a, b string) bool {
if len(a) != len(b) {
return false
}
@@ -463,7 +468,7 @@ func (pair pairKV) isValid() bool {
return pair.key.len > 0 || pair.value.len > 0
}
// isValidHeader is for the append-built [Header] store, where mustAppendSlice
// isValidHeader is for the append-built [HeaderV1] store, where mustAppendSlice
// burns byte 0 so a zero offset means absent. Drops offset-0 pairs otherwise.
func (pair pairKV) isValidHeader() bool { return pair.key.start > 0 }
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
)
@@ -48,7 +49,11 @@ var (
// returning the wrong bytes or panicking on a wrapped slice bound.
const maxBufLen = 0xffff
type headerBuf struct {
func protoIsV1(s string) bool {
return s[:min(len(strHTTP1), len(s))] == strHTTP1
}
type headerv1Buf struct {
kv kvBuffer
// buf[:len] holds entire HTTP header data, which may be normalized by [flags]. buf[off:len] holds data not yet processed during parsing.
// buf []byte
@@ -61,7 +66,7 @@ type headerBuf struct {
// reset sets the buffer data and discards all parsed data. The field table is
// grown to match the new buffer's capacity and never shrinks, so a header
// reused across requests settles on its largest buffer and stops allocating.
func (h *headerBuf) reset(buf []byte, numHeaderCapacity int) {
func (h *headerv1Buf) reset(buf []byte, numHeaderCapacity int) {
h.kv.Reset(buf, numHeaderCapacity)
h.off = 0
}
@@ -80,7 +85,7 @@ type scannerState struct {
initialized bool
}
func (h *Header) parse(asResponse bool) (err error) {
func (h *HeaderV1) parse(asResponse bool) (err error) {
debuglog("http:firstline:start")
err = h.parseFirstLine(asResponse)
if err != nil {
@@ -93,7 +98,7 @@ func (h *Header) parse(asResponse bool) (err error) {
return err
}
func (h *Header) parseFirstLine(asResponse bool) (err error) {
func (h *HeaderV1) parseFirstLine(asResponse bool) (err error) {
if len(h.hbuf.kv.buf) > maxBufLen {
return errBufferTooLarge // Offsets would overflow uint16 tokint.
}
@@ -107,7 +112,7 @@ func (h *Header) parseFirstLine(asResponse bool) (err error) {
return err
}
func (h *Header) parseNextHeaders(flags Flags) error {
func (h *HeaderV1) parseNextHeaders(flags Flags) error {
var ss scannerState
h.hbuf.parseNextHeaders(&ss, flags)
if ss.err != nil {
@@ -118,9 +123,9 @@ func (h *Header) parseNextHeaders(flags Flags) error {
return nil
}
func (hb *headerBuf) free() int { return hb.kv.free() }
func (hb *headerv1Buf) free() int { return hb.kv.free() }
func (hb *headerBuf) parseNextHeaders(ss *scannerState, flags Flags) {
func (hb *headerv1Buf) parseNextHeaders(ss *scannerState, flags Flags) {
debuglog("http:nexthdr:loop")
for kv := hb.next(ss); kv.isValidHeader(); kv = hb.next(ss) {
if !hb.kv.canAddOneKV() {
@@ -132,17 +137,17 @@ func (hb *headerBuf) parseNextHeaders(ss *scannerState, flags Flags) {
debuglog("http:nexthdr:done")
}
func (hb *headerBuf) offBuf() []byte {
func (hb *headerv1Buf) offBuf() []byte {
return hb.kv.buf[hb.off:]
}
func (hb *headerBuf) skipLeadingCRLF() {
func (hb *headerv1Buf) skipLeadingCRLF() {
for hb.off < len(hb.kv.buf) && (hb.kv.buf[hb.off] == '\n' || hb.kv.buf[hb.off] == '\r') {
hb.off++
}
}
func (hb *headerBuf) scanLine() []byte {
func (hb *headerv1Buf) scanLine() []byte {
buf := hb.scanUntilByte('\n')
if len(buf) > 0 && buf[len(buf)-1] == '\r' {
buf = buf[:len(buf)-1] // exclude carriage return.
@@ -153,7 +158,7 @@ func (hb *headerBuf) scanLine() []byte {
return buf
}
func (hb *headerBuf) scanUntilByte(c byte) []byte {
func (hb *headerv1Buf) scanUntilByte(c byte) []byte {
buf := hb.offBuf()
idx := bytes.IndexByte(buf, c)
if idx >= 0 {
@@ -163,7 +168,7 @@ func (hb *headerBuf) scanUntilByte(c byte) []byte {
return buf
}
func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto view, flags Flags, err error) {
func (hb *headerv1Buf) parseFirstLineRequest(initFlags Flags) (method, uri, proto view, flags Flags, err error) {
debuglog("http:req:scan")
hb.off = 0 // Parsing first line resets offset.
hb.skipLeadingCRLF()
@@ -183,21 +188,30 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto
reqURIEnd += methodEnd + 1
uri = hb.kv.view(b[methodEnd+1 : reqURIEnd])
proto = hb.kv.view(b[reqURIEnd+1:]) // Skip space before protocol.
if b2s(b[reqURIEnd+1:]) != strHTTP11 {
flags |= flagNoHTTP11
protoText := b2s(b[reqURIEnd+1:])
if !protoIsV1(protoText) {
// Refused here rather than after the fields: the field loop is nearly
// all of the parse cost and none of it serves a version this type
// does not speak. proto is set so the caller can name it, i.e: 505.
method = hb.kv.view(b[:methodEnd])
return method, uri, proto, flags | flagNoHTTP11, lneto.ErrUnsupported
} else if protoText != strHTTP11 {
flags |= flagNoHTTP11 // HTTP/1.0, which defaults to closing the connection.
}
} else if reqURIEnd == 0 {
return method, uri, proto, flags, errEmptyURI
} else {
// No version provided.
flags |= flagNoHTTP11
// No version at all is a HTTP/0.9 simple-request, not a 1.x request-line,
// RFC 9112 3. proto stays empty, telling it apart from a named version.
uri = hb.kv.view(b[methodEnd+1:])
method = hb.kv.view(b[:methodEnd])
return method, uri, proto, flags | flagNoHTTP11, lneto.ErrUnsupported
}
method = hb.kv.view(b[:methodEnd])
return method, uri, proto, flags, nil
}
func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, statusText view, flags Flags, err error) {
func (hb *headerv1Buf) parseFirstLineResponse(initFlags Flags) (statusCode, statusText view, flags Flags, err error) {
debuglog("http:resp:scan")
hb.off = 0 // Parsing first line resets offset.
hb.skipLeadingCRLF()
@@ -216,7 +230,11 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, status
if protoEnd < 0 {
return statusCode, statusText, flags, ErrNeedMoreData
}
if b2s(b[:protoEnd]) != strHTTP11 {
if !protoIsV1(b2s(b[:protoEnd])) {
// Refused before the fields, as on the request side: a response naming
// another version is not one this type can read.
return statusCode, statusText, flags | flagNoHTTP11, lneto.ErrUnsupported
} else if b2s(b[:protoEnd]) != strHTTP11 {
flags |= flagNoHTTP11
}
b = b[protoEnd+1:] // Advance past protocol and space.
@@ -243,7 +261,7 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, status
return statusCode, statusText, flags, nil
}
func (hb *headerBuf) next(ss *scannerState) pairKV {
func (hb *headerv1Buf) next(ss *scannerState) pairKV {
if !ss.initialized {
ss.nextColon = -1
ss.nextNewLine = -1
@@ -328,7 +346,7 @@ func (hb *headerBuf) next(ss *scannerState) pairKV {
}
// ConnectionClose returns true if 'Connection: close' header is set or if a invalid header was found.
func (h *Header) ConnectionClose() bool {
func (h *HeaderV1) ConnectionClose() bool {
flags := h.Flags()
closed := flags.HasAny(flagConnClose) ||
h.hasConnectionToken(strClose) ||
@@ -342,7 +360,7 @@ func (h *Header) ConnectionClose() bool {
// hasConnectionToken reports whether the Connection field lists token, which
// must be lowercase. The field name, its comma list and each token all compare
// case insensitively, RFC 9110 5.1 and 7.6.1.
func (h *Header) hasConnectionToken(token string) bool {
func (h *HeaderV1) hasConnectionToken(token string) bool {
value := h.GetFold(headerConnection)
for len(value) > 0 {
item := value
@@ -2,15 +2,18 @@ package httpraw
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/soypat/lneto"
)
func TestTryParse_IncrementalRequest(t *testing.T) {
// Full HTTP request split across multiple ReadFromBytes calls.
full := "GET /index.html HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/html\r\n\r\nbody here"
var hdr Header
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
// Feed data in small chunks to exercise incremental parsing.
chunks := splitInto(full, 10)
@@ -78,8 +81,8 @@ func TestTryParse_IncrementalRequest(t *testing.T) {
func TestTryParse_IncrementalResponse(t *testing.T) {
full := "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nServer: lneto\r\n\r\nhello"
var hdr Header
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
chunks := splitInto(full, 8)
var done bool
@@ -130,8 +133,8 @@ func TestReadFromLimited(t *testing.T) {
data := "GET / HTTP/1.1\r\nHost: test\r\n\r\n"
r := strings.NewReader(data)
var hdr Header
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
// Read in one shot.
n, err := hdr.ReadFromLimited(r, 256)
@@ -156,8 +159,8 @@ func TestReadFromLimited(t *testing.T) {
}
func TestReadFromLimited_MaxBytes(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
// Zero maxBytesToRead should error.
_, err := hdr.ReadFromLimited(strings.NewReader("data"), 0)
@@ -167,8 +170,8 @@ func TestReadFromLimited_MaxBytes(t *testing.T) {
}
func TestReadFromBytes_Empty(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
err := hdr.ReadFromBytes(nil)
if err == nil {
@@ -177,8 +180,8 @@ func TestReadFromBytes_Empty(t *testing.T) {
}
func TestBufferFreeAndCapacity(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 100), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 100), defaultKVCap)
if hdr.BufferCapacity() != 100 {
t.Errorf("capacity = %d; want 100", hdr.BufferCapacity())
@@ -194,9 +197,9 @@ func TestBufferFreeAndCapacity(t *testing.T) {
}
func TestEnableBufferGrowth(t *testing.T) {
var hdr Header
var hdr HeaderV1
buf := make([]byte, 0, 64)
hdr.Reset(buf, numHeaderCapacity)
hdr.Reset(buf, defaultKVCap)
hdr.ConfigBufferGrowth(false)
// With growth disabled, reading more than capacity should fail.
big := make([]byte, 128)
@@ -211,7 +214,7 @@ func TestEnableBufferGrowth(t *testing.T) {
func TestHeader_Add(t *testing.T) {
full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(false, []byte(full))
if err != nil {
t.Fatal(err)
@@ -238,7 +241,7 @@ func TestHeader_Add(t *testing.T) {
func TestHeader_SetBytes(t *testing.T) {
full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(false, []byte(full))
if err != nil {
t.Fatal(err)
@@ -254,7 +257,7 @@ func TestHeader_SetBytes(t *testing.T) {
func TestConnectionClose(t *testing.T) {
t.Run("HTTP11_NoConnectionHeader", func(t *testing.T) {
full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
hdr.ParseBytes(false, []byte(full))
if hdr.ConnectionClose() {
t.Error("HTTP/1.1 without Connection:close should not close")
@@ -263,7 +266,7 @@ func TestConnectionClose(t *testing.T) {
t.Run("ExplicitClose", func(t *testing.T) {
full := "GET / HTTP/1.1\r\nConnection: close\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
hdr.ParseBytes(false, []byte(full))
if !hdr.ConnectionClose() {
t.Error("Connection:close header should trigger close")
@@ -272,7 +275,7 @@ func TestConnectionClose(t *testing.T) {
t.Run("HTTP10_NoKeepAlive", func(t *testing.T) {
full := "GET / HTTP/1.0\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
hdr.ParseBytes(false, []byte(full))
if !hdr.ConnectionClose() {
t.Error("HTTP/1.0 without keep-alive should close")
@@ -281,7 +284,7 @@ func TestConnectionClose(t *testing.T) {
t.Run("HTTP10_KeepAlive", func(t *testing.T) {
full := "GET / HTTP/1.0\r\nConnection: keep-alive\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
hdr.ParseBytes(false, []byte(full))
if hdr.ConnectionClose() {
t.Error("HTTP/1.0 with keep-alive should not close")
@@ -291,7 +294,7 @@ func TestConnectionClose(t *testing.T) {
func TestTryParse_AlreadyParsed(t *testing.T) {
full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
hdr.ParseBytes(false, []byte(full))
// Calling TryParse again should return error.
@@ -303,7 +306,7 @@ func TestTryParse_AlreadyParsed(t *testing.T) {
func TestParseResponse_BadStatusCode(t *testing.T) {
full := "HTTP/1.1 abc Bad\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(true, []byte(full))
if err == nil {
t.Fatal("expected error for non-numeric status code")
@@ -370,7 +373,7 @@ func TestCookie_ForEach(t *testing.T) {
func TestHeader_MultilineValue(t *testing.T) {
// RFC 7230: obsolete line folding with \r\n followed by space/tab.
full := "GET / HTTP/1.1\r\nX-Multi: line1\r\n\tline2\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(false, []byte(full))
if err != nil {
t.Fatal(err)
@@ -386,8 +389,8 @@ func TestHeader_MultilineValue(t *testing.T) {
}
func TestHeader_ResponseRoundTrip(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.SetProtocol("HTTP/1.1")
hdr.SetStatus("404", "Not Found")
hdr.Add("Content-Type", "text/plain")
@@ -411,7 +414,7 @@ func TestHeader_ResponseRoundTrip(t *testing.T) {
}
// Parse back the generated response.
var hdr2 Header
var hdr2 HeaderV1
err = hdr2.ParseBytes(true, buf)
if err != nil {
t.Fatalf("re-parse response: %v", err)
@@ -426,8 +429,8 @@ func TestHeader_ResponseRoundTrip(t *testing.T) {
}
func TestHeader_RequestRoundTrip(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
var hdr HeaderV1
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.SetProtocol("HTTP/1.1")
hdr.SetMethod("POST")
hdr.SetRequestTarget("/api/data")
@@ -444,7 +447,7 @@ func TestHeader_RequestRoundTrip(t *testing.T) {
}
// Parse back the generated request.
var hdr2 Header
var hdr2 HeaderV1
err = hdr2.ParseBytes(false, buf)
if err != nil {
t.Fatalf("re-parse request: %v", err)
@@ -463,7 +466,7 @@ func TestHeader_RequestRoundTrip(t *testing.T) {
func TestParseResponse_StatusCodeOnly(t *testing.T) {
// Response with status code but no status text.
full := "HTTP/1.1 204\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(true, []byte(full))
if err != nil {
t.Fatal(err)
@@ -477,7 +480,7 @@ func TestParseResponse_StatusCodeOnly(t *testing.T) {
func TestParseResponse_HTTP10(t *testing.T) {
full := "HTTP/1.0 200 OK\r\nServer: old\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(true, []byte(full))
if err != nil {
t.Fatal(err)
@@ -492,12 +495,14 @@ func TestParseResponse_HTTP10(t *testing.T) {
}
func TestParseRequest_NoProtocol(t *testing.T) {
// HTTP/0.9 style: just method and URI, no version.
// HTTP/0.9 style: just method and URI, no version. Refused, but the
// request-line it did read stays readable so a caller can answer 400 and say
// what it saw.
full := "GET /simple\r\nHost: test\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(false, []byte(full))
if err != nil {
t.Fatal(err)
if !errors.Is(err, lneto.ErrUnsupported) {
t.Fatalf("want lneto.ErrUnsupported for a version-less request, got %v", err)
}
if string(hdr.Method()) != "GET" {
t.Errorf("method = %q; want GET", hdr.Method())
@@ -505,6 +510,8 @@ func TestParseRequest_NoProtocol(t *testing.T) {
if string(hdr.RequestTarget()) != "/simple" {
t.Errorf("URI = %q; want /simple", hdr.RequestTarget())
}
// An empty protocol is what tells HTTP/0.9 apart from a named version, which
// is how [httphi] picks 400 over 505.
if hdr.Protocol() != nil {
t.Errorf("protocol should be nil for version-less request, got %q", hdr.Protocol())
}
@@ -521,7 +528,7 @@ func TestCookie_QuotedValue(t *testing.T) {
func TestParseRequest_InvalidHeaderSpaceBeforeColon(t *testing.T) {
// RFC 7230 §3.2.4: No whitespace allowed between header name and colon.
full := "GET / HTTP/1.1\r\nBad Header : value\r\n\r\n"
var hdr Header
var hdr HeaderV1
err := hdr.ParseBytes(false, []byte(full))
if err == nil {
t.Fatal("expected error for space before colon in header name")
@@ -567,7 +574,7 @@ func TestConnectionCloseFolded(t *testing.T) {
{proto: "HTTP/1.0", field: "Host: h", wantClose: true},
} {
t.Run(test.proto+" "+test.field, func(t *testing.T) {
var hdr Header
var hdr HeaderV1
full := "GET / " + test.proto + "\r\nHost: h\r\n" + test.field + "\r\n\r\n"
if err := hdr.ParseBytes(false, []byte(full)); err != nil {
t.Fatal(err)
@@ -578,3 +585,63 @@ func TestConnectionCloseFolded(t *testing.T) {
})
}
}
// HeaderV1 speaks HTTP/1.x and nothing else, so a request or response naming
// another version is refused on the first line. That skips the field loop, which
// is where nearly all the parse cost is, and refuses the h2c preface a modern
// client opens with before it is mistaken for a request.
func TestHeaderV1RejectsUnsupportedVersion(t *testing.T) {
for _, test := range []struct {
name string
raw string
asResponse bool
}{
{name: "request http2", raw: "GET / HTTP/2.0\r\nHost: h\r\n\r\n"},
{name: "request http3", raw: "GET / HTTP/3.0\r\nHost: h\r\n\r\n"},
{name: "h2c preface", raw: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"},
{name: "request http09 no version", raw: "GET /index.html\r\nHost: h\r\n\r\n"},
{name: "request bogus proto", raw: "GET / BANANA\r\nHost: h\r\n\r\n"},
{name: "response http2", raw: "HTTP/2.0 200 OK\r\nServer: s\r\n\r\n", asResponse: true},
{name: "response http09", raw: "HTTP/0.9 200 OK\r\nServer: s\r\n\r\n", asResponse: true},
} {
t.Run(test.name, func(t *testing.T) {
var h HeaderV1
err := h.ParseBytes(test.asResponse, []byte(test.raw))
if !errors.Is(err, lneto.ErrUnsupported) {
t.Fatalf("want lneto.ErrUnsupported, got %v", err)
}
// The field loop must not have run: refusing early is the point.
fields := 0
h.ForEach(func(key, value []byte) bool { fields++; return true })
if fields != 0 {
t.Errorf("want no fields parsed, got %d", fields)
}
if h.ParsingSuccess() {
t.Error("a refused header must not report a successful parse")
}
})
}
}
// Both HTTP/1 versions stay supported: only non-1.x is refused.
func TestHeaderV1AcceptsV1Versions(t *testing.T) {
for _, test := range []struct {
raw string
asResponse bool
}{
{raw: "GET / HTTP/1.1\r\nHost: h\r\n\r\n"},
{raw: "GET / HTTP/1.0\r\nHost: h\r\n\r\n"},
{raw: "HTTP/1.1 200 OK\r\nServer: s\r\n\r\n", asResponse: true},
{raw: "HTTP/1.0 200 OK\r\nServer: s\r\n\r\n", asResponse: true},
} {
t.Run(test.raw[:12], func(t *testing.T) {
var h HeaderV1
if err := h.ParseBytes(test.asResponse, []byte(test.raw)); err != nil {
t.Fatalf("want parsed, got %v", err)
}
if !h.ParsingSuccess() {
t.Error("want a successful parse")
}
})
}
}