explicit header key/value alloc and add ExchangeConfig

This commit is contained in:
Patricio Whittingslow
2026-07-27 11:51:02 -03:00
parent 75d2c0c46d
commit 0e2f487e9f
10 changed files with 319 additions and 90 deletions
+2 -1
View File
@@ -41,8 +41,9 @@ var benchBody = []byte("hello world")
func benchExchange(b *testing.B, conn conn) *Exchange { func benchExchange(b *testing.B, conn conn) *Exchange {
b.Helper() b.Helper()
const bufferSize = 1024 const bufferSize = 1024
const numHeaderCap = 2
exch := new(Exchange) exch := new(Exchange)
exch.Configure(make([]byte, 2*bufferSize), bufferSize, false) exch.Configure(make([]byte, 2*bufferSize), bufferSize, numHeaderCap, false)
if !exch.Acquire(conn) { if !exch.Acquire(conn) {
b.Fatal("fresh exchange failed to acquire connection") b.Fatal("fresh exchange failed to acquire connection")
} }
+17 -11
View File
@@ -48,6 +48,13 @@ type Exchange struct {
readErr error readErr error
} }
type ExchangeConfig struct {
RawBuf []byte
RequestBufferLim int
NumHeaderCap int
NormalizeOutgoingKeys bool
}
// HijackRaw is a low-level implementation of http.Hijacker interface. // HijackRaw is a low-level implementation of http.Hijacker interface.
// A Hijack method is not exposed due to heap allocation implications and correctness concerns. // A Hijack method is not exposed due to heap allocation implications and correctness concerns.
// Below is what an actual implementation may look like: // Below is what an actual implementation may look like:
@@ -82,14 +89,15 @@ func (exch *Exchange) HijackRaw(dstBody []byte) (conn, []byte, error) {
// of which the first requestLim bytes are reserved for the request header. // of which the first requestLim bytes are reserved for the request header.
// Panics if requestLim exceeds the buffer. Set normalizeKeys to normalize // Panics if requestLim exceeds the buffer. Set normalizeKeys to normalize
// outgoing header keys, i.e: "content-type" to "Content-Type". // outgoing header keys, i.e: "content-type" to "Content-Type".
func (exch *Exchange) Configure(rawbuf []byte, requestLim int, normalizeKeys bool) { func (exch *Exchange) Configure(cfg ExchangeConfig) {
respSize := len(rawbuf) - requestLim respSize := len(cfg.RawBuf) - cfg.RequestBufferLim
if respSize < 0 { if respSize < 0 {
panic("request lim larger than buffer") panic("request lim larger than buffer")
} }
exch.rawbuf = rawbuf exch.rawbuf = cfg.RawBuf
exch.reqHdr.Reset(rawbuf[:0:requestLim]) exch.reqHdr.Reset(cfg.RawBuf[:0:cfg.RequestBufferLim], cfg.NumHeaderCap)
exch.normalizeKeys = normalizeKeys exch.reqHdr.ConfigBufferGrowth(false)
exch.normalizeKeys = cfg.NormalizeOutgoingKeys
} }
// Acquire claims the exchange for conn and resets it to serve a new request, // Acquire claims the exchange for conn and resets it to serve a new request,
@@ -111,7 +119,7 @@ func (exch *Exchange) Acquire(conn conn) bool {
exch.rw = conn exch.rw = conn
exch.headerWritten = false exch.headerWritten = false
exch.nextFree = nil exch.nextFree = nil
exch.reqHdr.Reset(nil) exch.reqHdr.Reset(nil, 0)
return true return true
} }
@@ -350,13 +358,11 @@ func (exch *Exchange) ReadBody(dst []byte) (n int, _ error) {
} }
n = copy(dst, toRead) n = copy(dst, toRead)
exch.respRemains -= n exch.respRemains -= n
if len(dst) == n { // hand over what already arrived since conn might have
// exhausted data and could block indefinetely.
return n, nil return n, nil
} }
dst = dst[n:] return exch.rw.Read(dst)
}
nr, err := exch.rw.Read(dst)
return nr + n, err
} }
func (exch *Exchange) remainingSurplusBody() ([]byte, error) { func (exch *Exchange) remainingSurplusBody() ([]byte, error) {
+155 -8
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"io" "io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"unsafe" "unsafe"
@@ -19,10 +20,11 @@ import (
func nopBackoff(consecutiveBackoffs uint) time.Duration { return lneto.BackoffFlagNop } func nopBackoff(consecutiveBackoffs uint) time.Duration { return lneto.BackoffFlagNop }
// newExchange returns an Exchange acquired on conn, ready to serve a request. // newExchange returns an Exchange acquired on conn, ready to serve a request.
func newExchange(t *testing.T, conn conn, bufferSize int, normalizeKeys bool) *Exchange { func newExchange(t *testing.T, conn conn, cfg ExchangeConfig) *Exchange {
t.Helper() t.Helper()
exch := new(Exchange) exch := new(Exchange)
exch.Configure(make([]byte, 2*bufferSize), bufferSize, normalizeKeys) const numHeaderCap = 1
exch.Configure(cfg)
if !exch.Acquire(conn) { if !exch.Acquire(conn) {
t.Fatal("fresh exchange failed to acquire connection") t.Fatal("fresh exchange failed to acquire connection")
} }
@@ -46,6 +48,7 @@ func serve(t *testing.T, request string, mux Mux) *rwconn {
// WriteHeader must emit a complete status line terminated in CRLF followed by // WriteHeader must emit a complete status line terminated in CRLF followed by
// the end-of-headers CRLF, for every status code including the longest text. // the end-of-headers CRLF, for every status code including the longest text.
func TestExchangeWriteHeader(t *testing.T) { func TestExchangeWriteHeader(t *testing.T) {
var buf [128]byte
for _, test := range []struct { for _, test := range []struct {
code int code int
want string want string
@@ -57,7 +60,7 @@ func TestExchangeWriteHeader(t *testing.T) {
{code: 511, want: "HTTP/1.1 511 Network Authentication Required\r\n\r\n"}, {code: 511, want: "HTTP/1.1 511 Network Authentication Required\r\n\r\n"},
} { } {
conn := newConn("") conn := newConn("")
exch := newExchange(t, conn, 128, false) exch := newExchange(t, conn, ExchangeConfig{RawBuf: buf[:], RequestBufferLim: 64})
exch.WriteHeader(test.code) exch.WriteHeader(test.code)
if got := conn.ViewWritten(); got != test.want { if got := conn.ViewWritten(); got != test.want {
t.Errorf("code %d: want %q, got %q", test.code, test.want, got) t.Errorf("code %d: want %q, got %q", test.code, test.want, got)
@@ -67,8 +70,9 @@ func TestExchangeWriteHeader(t *testing.T) {
// Status line is written once: a second WriteHeader must not reach the wire. // Status line is written once: a second WriteHeader must not reach the wire.
func TestExchangeWriteHeaderOnce(t *testing.T) { func TestExchangeWriteHeaderOnce(t *testing.T) {
var buf [128]byte
conn := newConn("") conn := newConn("")
exch := newExchange(t, conn, 128, false) exch := newExchange(t, conn, ExchangeConfig{RawBuf: buf[:], RequestBufferLim: 64})
exch.WriteHeader(404) exch.WriteHeader(404)
exch.WriteHeader(500) exch.WriteHeader(500)
const want = "HTTP/1.1 404 Not Found\r\n\r\n" const want = "HTTP/1.1 404 Not Found\r\n\r\n"
@@ -79,9 +83,10 @@ func TestExchangeWriteHeaderOnce(t *testing.T) {
// Write with no prior WriteHeader must flush a 200 header ahead of the body. // Write with no prior WriteHeader must flush a 200 header ahead of the body.
func TestExchangeWriteFlushesHeader(t *testing.T) { func TestExchangeWriteFlushesHeader(t *testing.T) {
var buf [128]byte
const body = "hello" const body = "hello"
conn := newConn("") conn := newConn("")
exch := newExchange(t, conn, 128, false) exch := newExchange(t, conn, ExchangeConfig{RawBuf: buf[:], RequestBufferLim: 64})
n, err := exch.WriteBody([]byte(body)) n, err := exch.WriteBody([]byte(body))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -124,7 +129,7 @@ func TestExchangeSetHeader(t *testing.T) {
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
conn := newConn("") conn := newConn("")
exch := newExchange(t, conn, 128, test.normalize) exch := newExchange(t, conn, ExchangeConfig{RawBuf: make([]byte, 128), RequestBufferLim: 64, NormalizeOutgoingKeys: test.normalize})
for _, kv := range test.set { for _, kv := range test.set {
if !exch.StageHeader(kv[0], kv[1]) { if !exch.StageHeader(kv[0], kv[1]) {
t.Fatalf("SetHeader(%q,%q) reported insufficient memory", kv[0], kv[1]) t.Fatalf("SetHeader(%q,%q) reported insufficient memory", kv[0], kv[1])
@@ -147,7 +152,7 @@ func TestExchangeSetHeader(t *testing.T) {
func TestExchangeSetHeaderOOM(t *testing.T) { func TestExchangeSetHeaderOOM(t *testing.T) {
const bufferSize = 32 const bufferSize = 32
conn := newConn("") conn := newConn("")
exch := newExchange(t, conn, bufferSize, false) exch := newExchange(t, conn, ExchangeConfig{RawBuf: make([]byte, bufferSize), RequestBufferLim: 64})
if exch.StageHeader("X-Big", strings.Repeat("v", 4*bufferSize)) { if exch.StageHeader("X-Big", strings.Repeat("v", 4*bufferSize)) {
t.Fatal("want insufficient memory reported for oversized header value") t.Fatal("want insufficient memory reported for oversized header value")
} }
@@ -346,10 +351,11 @@ func TestExchangeReadBody(t *testing.T) {
func TestExchangeStageOKAndFail(t *testing.T) { func TestExchangeStageOKAndFail(t *testing.T) {
const key, value = "K", "V" const key, value = "K", "V"
const field = len(key) + len(value) + len(":\r\n") const field = len(key) + len(value) + len(":\r\n")
const numHeaderCap = 4
for _, bufLen := range []int{field + 2, field + 1, field} { for _, bufLen := range []int{field + 2, field + 1, field} {
conn := newConn("") conn := newConn("")
exch := new(Exchange) exch := new(Exchange)
exch.Configure(make([]byte, bufLen), bufLen, false) exch.Configure(make([]byte, bufLen), bufLen, numHeaderCap, false)
if !exch.Acquire(conn) { if !exch.Acquire(conn) {
t.Fatal("fresh exchange failed to acquire connection") t.Fatal("fresh exchange failed to acquire connection")
} }
@@ -942,3 +948,144 @@ func TestExchangeRequestParseMultipartRejects(t *testing.T) {
} }
} }
} }
// A request from a browser carries around twenty header fields. Serving one
// must not depend on how many fields the parser happens to have room for: the
// exchange's buffer is the memory the caller granted, and the field table comes
// out of it.
func TestHandleBrowserSizedRequest(t *testing.T) {
const wantMode = "navigate"
request := "GET /echo HTTP/1.1\r\nHost: lneto.test\r\n" +
"User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n" +
"Accept-Language: en-US,en;q=0.5\r\nAccept-Encoding: gzip, deflate, br\r\n" +
"Upgrade-Insecure-Requests: 1\r\nSec-Fetch-Dest: document\r\nSec-Fetch-Site: none\r\n" +
"Sec-Ch-Ua: \"Chromium\";v=\"120\"\r\nCache-Control: max-age=0\r\nDnt: 1\r\n" +
"Referer: https://lneto.test/index.html\r\nCookie: session=abcdef0123456789; theme=dark\r\n" +
"X-Trace: 0123456789abcdef\r\nX-Client: bench\r\nX-Seq: 42\r\nX-Tag: alpha\r\n" +
"X-Nonce: cafebabe\r\nX-Mode: " + wantMode + "\r\n\r\n"
var gotMode string
var fields int
var sm MuxSlice
sm.Reset(1)
sm.Handle("GET /echo", func(exch *Exchange) {
gotMode = string(exch.RequestHeader("X-Mode"))
exch.RequestHeaderRaw().ForEach(func(key, value []byte) error {
fields++
return nil
})
})
conn := newConn(request)
conn.Hangup()
exch := newExchange(t, conn, 8192, false)
if err := Handle(exch, &sm, nopBackoff); err != nil {
t.Fatalf("serving a browser sized request: %s", err)
}
if gotMode != wantMode {
t.Errorf("last header field read back as %q, want %q", gotMode, wantMode)
}
const sent = 19
if fields < sent {
t.Errorf("handler saw %d header fields, request carried %d", fields, sent)
}
}
// A request with more header fields than the exchange has room for must be
// answered, not dropped: the peer learns its request was too large instead of
// seeing the connection go away.
func TestHandleTooManyHeaderFields(t *testing.T) {
request := "GET /echo HTTP/1.1\r\nHost: lneto.test\r\n"
for i := 0; i < 512; i++ {
request += "H" + strconv.Itoa(i) + ":v\r\n"
}
request += "\r\n"
var served bool
var sm MuxSlice
sm.Reset(1)
sm.Handle("GET /echo", func(exch *Exchange) { served = true })
conn := newConn(request)
conn.Hangup()
exch := newExchange(t, conn, 1024, false)
err := Handle(exch, &sm, nopBackoff)
if err != nil {
t.Fatal(err)
}
if served {
t.Fatal("handler ran on a request the parser could not hold")
}
got := conn.ViewWritten()
if !strings.HasPrefix(got, "HTTP/1.1 431 ") {
t.Errorf("want a 431 answer, got %q", firstLine(got))
}
}
func firstLine(s string) string {
if i := strings.Index(s, "\r\n"); i >= 0 {
return s[:i]
}
return s
}
// A body that already arrived alongside the request header must be handed over
// without touching the connection again. A peer that sent a whole request and
// is waiting for its answer sends nothing more, so a read for bytes already in
// hand blocks until the connection's deadline, or forever without one.
func TestExchangeReadBodyDoesNotReadPastWhatArrived(t *testing.T) {
const body = "message body"
dst := make([]byte, 64) // Deliberately larger than the body.
var got string
var readErr error
var sm MuxSlice
sm.Reset(1)
sm.Handle("POST /", func(ex *Exchange) {
n, err := ex.ReadBody(dst)
got, readErr = string(dst[:n]), err
ex.WriteHeader(200)
})
// The peer is still there, waiting to be answered: a read for bytes it is
// not going to send blocks, exactly as it does on a socket.
conn := &blockingConn{request: "POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 12\r\n\r\n" + body}
exch := newExchange(t, conn, 1024, false)
done := make(chan struct{})
go func() {
defer close(done)
Handle(exch, &sm, nopBackoff)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("ReadBody blocked waiting for a body that had already arrived")
}
if readErr != nil {
t.Fatal(readErr)
}
if got != body {
t.Errorf("want body %q, got %q", body, got)
}
}
// blockingConn delivers a request and then blocks on reads, the way a peer
// awaiting its answer does. Writes are discarded.
type blockingConn struct {
request string
read int
blocked chan struct{}
}
func (c *blockingConn) Read(b []byte) (int, error) {
if c.read >= len(c.request) {
if c.blocked == nil {
c.blocked = make(chan struct{})
}
<-c.blocked // Nothing more is coming, and nothing unblocks this.
return 0, io.EOF
}
n := copy(b, c.request[c.read:])
c.read += n
return n, nil
}
func (c *blockingConn) Write(b []byte) (int, error) { return len(b), nil }
func (c *blockingConn) Close() error { return nil }
+8 -1
View File
@@ -5,6 +5,7 @@ import (
"unsafe" "unsafe"
"github.com/soypat/lneto" "github.com/soypat/lneto"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal" "github.com/soypat/lneto/internal"
) )
@@ -13,7 +14,7 @@ import (
// Handle does not close the connection on any outcome: the caller owns it. // Handle does not close the connection on any outcome: the caller owns it.
func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error { func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
reqhdr := &exch.reqHdr reqhdr := &exch.reqHdr
reqhdr.Reset(nil) reqhdr.Reset(nil, 0) // Assume exchange has been configured and reuse memory.
var consecutiveBackoffs uint var consecutiveBackoffs uint
for { for {
n, err := reqhdr.ReadFromLimited(exch.rw, reqhdr.BufferFree()) n, err := reqhdr.ReadFromLimited(exch.rw, reqhdr.BufferFree())
@@ -31,6 +32,12 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
if needMore { if needMore {
continue // Request header split across reads, accumulate the rest. continue // Request header split across reads, accumulate the rest.
} else if err != nil { } else if err != nil {
if err == httpraw.ErrHeaderTooMany || err == httpraw.ErrSmallHeaderBuffer {
// The peer is owed an answer: no larger buffer is coming, so
// say so instead of dropping the connection, RFC 6585 5.
exch.StageHeader("Content-Length", "0")
exch.WriteHeader(int(StatusRequestHeaderFieldsTooLarge))
}
return err return err
} }
break // Done! break // Done!
+18 -3
View File
@@ -49,6 +49,7 @@ type Router struct {
numGoro int numGoro int
reqBuf int reqBuf int
respBuf int respBuf int
reqNumHeaderCap int
normalizeKeys bool normalizeKeys bool
pendingConns chan job pendingConns chan job
mux Mux mux Mux
@@ -79,6 +80,8 @@ type RouterConfig struct {
// "HTTP/1.1 200 OK\r\n" does not count towards this memory, only actual Headers key/value pairs use this memory. // "HTTP/1.1 200 OK\r\n" does not count towards this memory, only actual Headers key/value pairs use this memory.
// After memory is fully consumed [Exchange.StageHeader] will not append more headers. // After memory is fully consumed [Exchange.StageHeader] will not append more headers.
ResponseHeaderMinBufferSize int ResponseHeaderMinBufferSize int
// Number of request header key/value pairs to parse before failing and returning [StatusRequestHeaderFieldsTooLarge].
RequestNumHeaderCap int
// NormalizeOutgoingKeys normalizes response header field keys as they are // NormalizeOutgoingKeys normalizes response header field keys as they are
// staged, i.e: "content-type" becomes "Content-Type". // staged, i.e: "content-type" becomes "Content-Type".
@@ -154,6 +157,7 @@ func (r *Router) Configure(cfg RouterConfig) error {
gen := r.gen.Load() gen := r.gen.Load()
numgoro := cfg.FixedNumGoroutines numgoro := cfg.FixedNumGoroutines
workerMode := cfg.workerMode() workerMode := cfg.workerMode()
r.reqNumHeaderCap = cfg.RequestNumHeaderCap
r.reqBuf = cfg.RequestHeaderBufferSize r.reqBuf = cfg.RequestHeaderBufferSize
r.respBuf = cfg.ResponseHeaderMinBufferSize r.respBuf = cfg.ResponseHeaderMinBufferSize
r.mux = cfg.Mux r.mux = cfg.Mux
@@ -183,7 +187,13 @@ func (r *Router) Configure(cfg RouterConfig) error {
for i := range numgoro { for i := range numgoro {
// TODO exchange buffer alloc // TODO exchange buffer alloc
goff := i * rawBuflen goff := i * rawBuflen
r.exchs[i].Configure(r.globbuf[goff:goff+rawBuflen], cfg.RequestHeaderBufferSize, cfg.NormalizeOutgoingKeys) // r.globbuf[goff:goff+rawBuflen], cfg.RequestHeaderBufferSize, cfg.RequestNumHeaderCap, cfg.NormalizeOutgoingKeys
r.exchs[i].Configure(ExchangeConfig{
RawBuf: r.globbuf[goff : goff+rawBuflen],
RequestBufferLim: cfg.RequestHeaderBufferSize,
NumHeaderCap: cfg.RequestNumHeaderCap,
NormalizeOutgoingKeys: cfg.NormalizeOutgoingKeys,
})
go r.goroWorker(gen, jobqueue, cfg.Backoff, cfg.Mux) go r.goroWorker(gen, jobqueue, cfg.Backoff, cfg.Mux)
} }
r.pendingConns = jobqueue r.pendingConns = jobqueue
@@ -324,9 +334,14 @@ func (r *Router) getExchLocked(conn conn) (exch *Exchange) {
} }
} }
if r.numGoro == 0 { if r.numGoro == -1 {
exch := new(Exchange) exch := new(Exchange)
exch.Configure(make([]byte, r.respBuf+r.reqBuf), r.reqBuf, r.normalizeKeys) exch.Configure(ExchangeConfig{
RawBuf: make([]byte, r.respBuf+r.reqBuf),
RequestBufferLim: r.reqBuf,
NumHeaderCap: r.reqNumHeaderCap,
NormalizeOutgoingKeys: r.normalizeKeys,
})
exch.Acquire(conn) // Fresh exchange, CAS cannot fail. exch.Acquire(conn) // Fresh exchange, CAS cannot fail.
return exch return exch
} }
+19 -17
View File
@@ -65,10 +65,12 @@ type Header struct {
// Flags returns [Flags] to signal status code has been set, Connection:Close or other useful signals provided by flags. // 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.flags } func (h *Header) Flags() Flags { return h.flags }
// EnableBufferGrowth disables buffer growth during parsing if b is false. Is enabled by default. // ConfigBufferGrowth configures the memory the header may use. Setting
// Disabling buffer growth prevents allocations but methods may throw errors on insufficient memory. // outlives [Header.Reset]. Call before parsing/reading.
func (h *Header) EnableBufferGrowth(b bool) { //
if !b { // enableBufferGrowth enables growing both the header buffer and the header key/value pair slice.
func (h *Header) ConfigBufferGrowth(enableBufferGrowth bool) {
if !enableBufferGrowth {
h.flags |= flagNoBufferGrow h.flags |= flagNoBufferGrow
} else { } else {
h.flags &^= flagNoBufferGrow h.flags &^= flagNoBufferGrow
@@ -77,7 +79,7 @@ func (h *Header) EnableBufferGrowth(b bool) {
// ParseBytes copies the bytes into buffer and parses the HTTP header. It fails if HTTP header data is incomplete. // 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 *Header) ParseBytes(asResponse bool, b []byte) error {
h.Reset(nil) h.Reset(nil, 0)
h.hbuf.readFromBytes(b) h.hbuf.readFromBytes(b)
return h.parse(asResponse) return h.parse(asResponse)
} }
@@ -86,7 +88,7 @@ func (h *Header) ParseBytes(asResponse bool, b []byte) error {
// It fails if HTTP data is incomplete. // It fails if HTTP data is incomplete.
func (h *Header) Parse(asResponse bool) error { func (h *Header) Parse(asResponse bool) error {
debuglog("http:parse:reset") debuglog("http:parse:reset")
h.Reset(h.hbuf.buf) h.Reset(h.hbuf.buf, 0)
debuglog("http:parse:start") debuglog("http:parse:start")
return h.parse(asResponse) return h.parse(asResponse)
} }
@@ -119,7 +121,7 @@ func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) {
return err == ErrNeedMoreData, err return err == ErrNeedMoreData, err
} }
} }
err = h.parseNextHeaders() err = h.parseNextHeaders(h.flags)
return err == ErrNeedMoreData, err return err == ErrNeedMoreData, err
} }
@@ -133,7 +135,7 @@ func (h *Header) ParsingSuccess() bool {
// If read is successful (read length>0) and reader returns [io.EOF] then ReadFromLimited will return a nil error. // 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 *Header) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) {
if maxBytesToRead <= 0 { if maxBytesToRead <= 0 {
return 0, errSmallBuffer return 0, ErrSmallHeaderBuffer
} else if h.flags.HasAny(flagMangledBuffer) { } else if h.flags.HasAny(flagMangledBuffer) {
return 0, errMangledBuffer return 0, errMangledBuffer
} else if h.flags.HasAny(flagReaderEOF) { } else if h.flags.HasAny(flagReaderEOF) {
@@ -142,14 +144,14 @@ func (h *Header) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) {
free := h.BufferFree() free := h.BufferFree()
if free < maxBytesToRead { if free < maxBytesToRead {
if h.flags.HasAny(flagNoBufferGrow) { if h.flags.HasAny(flagNoBufferGrow) {
return 0, errSmallBuffer return 0, ErrSmallHeaderBuffer
} }
h.hbuf.buf = slices.Grow(h.hbuf.buf, maxBytesToRead) h.hbuf.buf = slices.Grow(h.hbuf.buf, maxBytesToRead)
} }
blen := len(h.hbuf.buf) blen := len(h.hbuf.buf)
b := h.hbuf.buf[blen:min(blen+maxBytesToRead, cap(h.hbuf.buf))] b := h.hbuf.buf[blen:min(blen+maxBytesToRead, cap(h.hbuf.buf))]
if len(b) == 0 { if len(b) == 0 {
return 0, errSmallBuffer return 0, ErrSmallHeaderBuffer
} }
n, err := r.Read(b) n, err := r.Read(b)
if err != nil && err == io.EOF { if err != nil && err == io.EOF {
@@ -166,12 +168,12 @@ func (h *Header) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) {
// Used to accumulate HTTP header for later parsing with [Header.TryParse]. // Used to accumulate HTTP header for later parsing with [Header.TryParse].
func (h *Header) ReadFromBytes(b []byte) (int, error) { func (h *Header) ReadFromBytes(b []byte) (int, error) {
if len(b) == 0 { if len(b) == 0 {
return 0, errSmallBuffer return 0, ErrSmallHeaderBuffer
} }
free := h.BufferFree() free := h.BufferFree()
if free < len(b) { if free < len(b) {
if h.flags.HasAny(flagNoBufferGrow) { if h.flags.HasAny(flagNoBufferGrow) {
return 0, errSmallBuffer return 0, ErrSmallHeaderBuffer
} }
h.hbuf.buf = slices.Grow(h.hbuf.buf, len(b)) h.hbuf.buf = slices.Grow(h.hbuf.buf, len(b))
} }
@@ -253,13 +255,13 @@ func (hb *headerBuf) forEach(cb func(key, value []byte) error) error {
// h.Reset(prealloc[:0]); h.ParseBytes(httpHeader) // Tell header to use a pre-allocated buffer capacity. // h.Reset(prealloc[:0]); h.ParseBytes(httpHeader) // Tell header to use a pre-allocated buffer capacity.
// h.Reset(httpHeader); h.Parse() // Parse bytes in place with no copying. // h.Reset(httpHeader); h.Parse() // Parse bytes in place with no copying.
// h.Reset(nil) // Reuse buffer previously set in a call to Reset. // h.Reset(nil) // Reuse buffer previously set in a call to Reset.
func (h *Header) Reset(buf []byte) { func (h *Header) Reset(buf []byte, numHeaderCapacity int) {
if h.flags.HasAny(flagNoBufferGrow) && cap(buf) < 32 {
panic("small buffer and flagNoBufferGrow set")
}
const persistentFlags = flagNoBufferGrow const persistentFlags = flagNoBufferGrow
debuglog("http:reset:hbuf") debuglog("http:reset:hbuf")
h.hbuf.reset(buf) h.hbuf.reset(buf, numHeaderCapacity)
if h.flags.HasAny(flagNoBufferGrow) && cap(h.hbuf.buf) < 32 {
panic("small buffer and flagNoBufferGrow set")
}
*h = Header{ *h = Header{
hbuf: h.hbuf, hbuf: h.hbuf,
flags: h.flags & persistentFlags, flags: h.flags & persistentFlags,
+53 -10
View File
@@ -2,6 +2,7 @@ package httpraw
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@@ -10,6 +11,8 @@ import (
"time" "time"
) )
const numHeaderCapacity = 16
func TestHeaderParseRequest(t *testing.T) { func TestHeaderParseRequest(t *testing.T) {
const ( const (
wantMethod = "GET" wantMethod = "GET"
@@ -387,7 +390,7 @@ func TestCopyDecodedPercentURLInPlace(t *testing.T) {
func TestHeaderSetOverwrite(t *testing.T) { func TestHeaderSetOverwrite(t *testing.T) {
var h Header var h Header
h.Reset(nil) h.Reset(nil, numHeaderCapacity)
h.SetMethod("GET") h.SetMethod("GET")
h.SetRequestTarget("/") h.SetRequestTarget("/")
h.SetProtocol("HTTP/1.1") h.SetProtocol("HTTP/1.1")
@@ -410,7 +413,7 @@ func TestHeaderSetOverwrite(t *testing.T) {
func TestHeaderSetBytesEmptyValue(t *testing.T) { func TestHeaderSetBytesEmptyValue(t *testing.T) {
var h Header var h Header
h.Reset(nil) h.Reset(nil, numHeaderCapacity)
h.SetBytes("X-Empty", nil) h.SetBytes("X-Empty", nil)
if got := h.Get("X-Empty"); len(got) != 0 { if got := h.Get("X-Empty"); len(got) != 0 {
t.Errorf("want empty value, got %q", got) t.Errorf("want empty value, got %q", got)
@@ -465,7 +468,7 @@ func TestHeader_SplitBeforeColonStillParses(t *testing.T) {
const part2 = ": example.com\r\n\r\n" const part2 = ": example.com\r\n\r\n"
var h Header var h Header
h.Reset(nil) h.Reset(nil, numHeaderCapacity)
if _, err := h.ReadFromBytes([]byte(part1)); err != nil { if _, err := h.ReadFromBytes([]byte(part1)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -498,7 +501,7 @@ func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) {
const key, value = "K", "V" const key, value = "K", "V"
buf := make([]byte, 0, len(key)+len(value)) // exact cap, no slack. buf := make([]byte, 0, len(key)+len(value)) // exact cap, no slack.
var h Header var h Header
h.Reset(buf) h.Reset(buf, numHeaderCapacity)
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
t.Fatalf("appendHeader panicked on exact-cap buffer: %v", r) t.Fatalf("appendHeader panicked on exact-cap buffer: %v", r)
@@ -515,8 +518,8 @@ func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) {
func TestHeader_AddFullBufferNoPanic(t *testing.T) { func TestHeader_AddFullBufferNoPanic(t *testing.T) {
buf := make([]byte, 0, 40) // Small cap; enough for Reset (len 0) but not the field below. buf := make([]byte, 0, 40) // Small cap; enough for Reset (len 0) but not the field below.
var h Header var h Header
h.Reset(buf) h.Reset(buf, numHeaderCapacity)
h.EnableBufferGrowth(false) h.ConfigBufferGrowth(false)
h.SetMethod("GET") h.SetMethod("GET")
h.SetRequestTarget("/") h.SetRequestTarget("/")
h.SetProtocol("HTTP/1.1") h.SetProtocol("HTTP/1.1")
@@ -550,7 +553,7 @@ func TestHeader_SetInt(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var h Header var h Header
h.Reset(nil) h.Reset(nil, numHeaderCapacity)
h.SetInt("Content-Length", tc.value, tc.base) h.SetInt("Content-Length", tc.value, tc.base)
if got := string(h.Get("Content-Length")); got != tc.want { if got := string(h.Get("Content-Length")); got != tc.want {
t.Fatalf("want %q, got %q", tc.want, got) t.Fatalf("want %q, got %q", tc.want, got)
@@ -562,7 +565,7 @@ func TestHeader_SetInt(t *testing.T) {
// SetInt on an existing key must reuse the slot in place (single field, latest value). // SetInt on an existing key must reuse the slot in place (single field, latest value).
func TestHeader_SetIntOverwrite(t *testing.T) { func TestHeader_SetIntOverwrite(t *testing.T) {
var h Header var h Header
h.Reset(nil) h.Reset(nil, numHeaderCapacity)
h.SetMethod("GET") h.SetMethod("GET")
h.SetRequestTarget("/") h.SetRequestTarget("/")
h.SetProtocol("HTTP/1.1") h.SetProtocol("HTTP/1.1")
@@ -586,8 +589,8 @@ func TestHeader_SetIntOverwrite(t *testing.T) {
func TestHeader_SetIntNoAlloc(t *testing.T) { func TestHeader_SetIntNoAlloc(t *testing.T) {
buf := make([]byte, 0, 256) buf := make([]byte, 0, 256)
var h Header var h Header
h.Reset(buf) h.Reset(buf, numHeaderCapacity)
h.EnableBufferGrowth(false) h.ConfigBufferGrowth(false)
h.Add("Content-Length", "0000000000000000000000") // pre-size a reusable slot. h.Add("Content-Length", "0000000000000000000000") // pre-size a reusable slot.
allocs := testing.AllocsPerRun(100, func() { allocs := testing.AllocsPerRun(100, func() {
h.SetInt("Content-Length", 1234567890, 10) h.SetInt("Content-Length", 1234567890, 10)
@@ -599,3 +602,43 @@ func TestHeader_SetIntNoAlloc(t *testing.T) {
t.Fatalf("want %q, got %q", "1234567890", got) t.Fatalf("want %q, got %q", "1234567890", got)
} }
} }
// A browser sends upwards of twenty header fields and an API client with a few
// custom fields is not far behind. The field table must be sized from the
// buffer the caller handed over, not fixed at a count that a real request
// exceeds.
func TestHeader_FieldTableSizedFromBuffer(t *testing.T) {
const wantVal = "the-canary-value"
raw := "GET / HTTP/1.1\r\nHost: lneto.test\r\n"
for i := 0; i < 40; i++ {
raw += "X-Field-" + strconv.Itoa(i) + ": value-of-a-realistic-length-here\r\n"
}
raw += "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.
err := h.ParseBytes(false, []byte(raw))
if err != nil {
t.Fatalf("parsing a 42 field request into an 8kB buffer: %s", err)
}
if got := string(h.Get("X-Canary")); got != wantVal {
t.Fatalf("want X-Canary %q, got %q", wantVal, got)
}
}
// A buffer too small for the fields it is handed must be refused with an error
// the caller can act on, so a server answers 431 instead of dropping the peer.
func TestHeader_FieldTableFullIsReported(t *testing.T) {
raw := "GET / HTTP/1.1\r\n"
for i := 0; i < 64; i++ {
raw += "H" + strconv.Itoa(i) + ":v\r\n" // As short as a field gets.
}
raw += "\r\n"
var h Header
h.Reset(make([]byte, 0, 512), numHeaderCapacity)
h.ConfigBufferGrowth(false)
err := h.ParseBytes(false, []byte(raw))
if !errors.Is(err, ErrHeaderTooMany) {
t.Fatalf("want ErrHeaderFieldsTooLarge, got %v", err)
}
}
+24 -15
View File
@@ -18,8 +18,13 @@ var (
errNoBoundary = errors.New("httpraw: multipart boundary not set") errNoBoundary = errors.New("httpraw: multipart boundary not set")
errUnparsed = errors.New("need to finish parsing") errUnparsed = errors.New("need to finish parsing")
errInvalidName = errors.New("invalid header name") errInvalidName = errors.New("invalid header name")
errSmallBuffer = errors.New("small read buffer. Increase ReadBufferSize") ErrSmallHeaderBuffer = errors.New("httpraw: Header buffer exhausted, increase size")
errOOM = errors.New("httpraw: buffer out of memory") errOOM = errors.New("httpraw: Header incomplete due to OOM")
// ErrHeaderTooMany signals a header block carrying more fields than
// the buffer it is parsed into has room for, see [Header.Reset]. A server
// answers it with 431, RFC 6585 5: no larger buffer is coming, so reading
// the rest of the block would only spend memory on a request already lost.
ErrHeaderTooMany = errors.New("httpraw: more header fields than buffer holds")
// Header.Set and Header.Add mangles the buffer. // Header.Set and Header.Add mangles the buffer.
// Call them after retrieving the Body. Do not call them before parsing the header (why would you even do that?). // Call them after retrieving the Body. Do not call them before parsing the header (why would you even do that?).
errMangledBuffer = errors.New("httpraw: mangled buffer") errMangledBuffer = errors.New("httpraw: mangled buffer")
@@ -52,17 +57,21 @@ type headerBuf struct {
headers []argsKV headers []argsKV
} }
// reset sets the buffer data and discards all parsed data. // reset sets the buffer data and discards all parsed data. The field table is
func (h *headerBuf) reset(buf []byte) { // 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) {
if buf == nil { if buf == nil {
buf = h.buf[:0] // Reuse buffer but discard raw data on nil input. buf = h.buf[:0] // Reuse buffer but discard raw data on nil input.
} }
if cap(h.headers) == 0 { if numHeaderCapacity != 0 {
h.headers = make([]argsKV, 16) internal.SliceReuse(&h.headers, numHeaderCapacity)
} else {
h.headers = h.headers[:0]
} }
*h = headerBuf{ *h = headerBuf{
buf: buf, buf: buf,
headers: h.headers[:0], headers: h.headers,
} }
} }
@@ -100,7 +109,7 @@ func (h *Header) parse(asResponse bool) (err error) {
return err return err
} }
debuglog("http:firstline:done") debuglog("http:firstline:done")
err = h.parseNextHeaders() err = h.parseNextHeaders(h.flags)
debuglog("http:headers:done") debuglog("http:headers:done")
return err return err
} }
@@ -117,9 +126,9 @@ func (h *Header) parseFirstLine(asResponse bool) (err error) {
return err return err
} }
func (h *Header) parseNextHeaders() error { func (h *Header) parseNextHeaders(flags Flags) error {
var ss scannerState var ss scannerState
h.hbuf.parseNextHeaders(&ss) h.hbuf.parseNextHeaders(&ss, flags)
if ss.err != nil { if ss.err != nil {
h.flags |= flagConnClose h.flags |= flagConnClose
return ss.err return ss.err
@@ -134,13 +143,13 @@ func (hb *headerBuf) readFromBytes(b []byte) {
func (hb *headerBuf) free() int { return cap(hb.buf) - len(hb.buf) } func (hb *headerBuf) free() int { return cap(hb.buf) - len(hb.buf) }
func (hb *headerBuf) parseNextHeaders(ss *scannerState) { func (hb *headerBuf) parseNextHeaders(ss *scannerState, flags Flags) {
debuglog("http:nexthdr:loop") debuglog("http:nexthdr:loop")
for kv := hb.next(ss); kv.isValid(); kv = hb.next(ss) { for kv := hb.next(ss); kv.isValid(); kv = hb.next(ss) {
if len(hb.headers) == cap(hb.headers) { if len(hb.headers) == cap(hb.headers) && flags.HasAny(flagNoBufferGrow) {
// Refuse to grow the headers slice: caller must pre-allocate // Refuse to grow the headers slice: the caller granted this much
// sufficient capacity via reset or use a larger initial size. // memory and no more, see [Header.Reset].
ss.err = errOOM ss.err = ErrHeaderTooMany
return return
} }
hb.headers = append(hb.headers, kv) hb.headers = append(hb.headers, kv)
+10 -11
View File
@@ -10,7 +10,7 @@ func TestTryParse_IncrementalRequest(t *testing.T) {
// Full HTTP request split across multiple ReadFromBytes calls. // 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" full := "GET /index.html HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/html\r\n\r\nbody here"
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 256)) hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
// Feed data in small chunks to exercise incremental parsing. // Feed data in small chunks to exercise incremental parsing.
chunks := splitInto(full, 10) chunks := splitInto(full, 10)
@@ -85,7 +85,7 @@ func TestTryParse_IncrementalRequest(t *testing.T) {
func TestTryParse_IncrementalResponse(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" full := "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nServer: lneto\r\n\r\nhello"
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 256)) hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
chunks := splitInto(full, 8) chunks := splitInto(full, 8)
var done bool var done bool
@@ -137,7 +137,7 @@ func TestReadFromLimited(t *testing.T) {
r := strings.NewReader(data) r := strings.NewReader(data)
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 256)) hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
// Read in one shot. // Read in one shot.
n, err := hdr.ReadFromLimited(r, 256) n, err := hdr.ReadFromLimited(r, 256)
@@ -163,7 +163,7 @@ func TestReadFromLimited(t *testing.T) {
func TestReadFromLimited_MaxBytes(t *testing.T) { func TestReadFromLimited_MaxBytes(t *testing.T) {
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 256)) hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
// Zero maxBytesToRead should error. // Zero maxBytesToRead should error.
_, err := hdr.ReadFromLimited(strings.NewReader("data"), 0) _, err := hdr.ReadFromLimited(strings.NewReader("data"), 0)
@@ -174,7 +174,7 @@ func TestReadFromLimited_MaxBytes(t *testing.T) {
func TestReadFromBytes_Empty(t *testing.T) { func TestReadFromBytes_Empty(t *testing.T) {
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 256)) hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
_, err := hdr.ReadFromBytes(nil) _, err := hdr.ReadFromBytes(nil)
if err == nil { if err == nil {
@@ -184,7 +184,7 @@ func TestReadFromBytes_Empty(t *testing.T) {
func TestBufferFreeAndCapacity(t *testing.T) { func TestBufferFreeAndCapacity(t *testing.T) {
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 100)) hdr.Reset(make([]byte, 0, 100), numHeaderCapacity)
if hdr.BufferCapacity() != 100 { if hdr.BufferCapacity() != 100 {
t.Errorf("capacity = %d; want 100", hdr.BufferCapacity()) t.Errorf("capacity = %d; want 100", hdr.BufferCapacity())
@@ -202,9 +202,8 @@ func TestBufferFreeAndCapacity(t *testing.T) {
func TestEnableBufferGrowth(t *testing.T) { func TestEnableBufferGrowth(t *testing.T) {
var hdr Header var hdr Header
buf := make([]byte, 0, 64) buf := make([]byte, 0, 64)
hdr.Reset(buf) hdr.Reset(buf, numHeaderCapacity)
hdr.EnableBufferGrowth(false) hdr.ConfigBufferGrowth(false)
// With growth disabled, reading more than capacity should fail. // With growth disabled, reading more than capacity should fail.
big := make([]byte, 128) big := make([]byte, 128)
for i := range big { for i := range big {
@@ -389,7 +388,7 @@ func TestHeader_MultilineValue(t *testing.T) {
func TestHeader_ResponseRoundTrip(t *testing.T) { func TestHeader_ResponseRoundTrip(t *testing.T) {
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 256)) hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
hdr.SetProtocol("HTTP/1.1") hdr.SetProtocol("HTTP/1.1")
hdr.SetStatus("404", "Not Found") hdr.SetStatus("404", "Not Found")
hdr.Add("Content-Type", "text/plain") hdr.Add("Content-Type", "text/plain")
@@ -429,7 +428,7 @@ func TestHeader_ResponseRoundTrip(t *testing.T) {
func TestHeader_RequestRoundTrip(t *testing.T) { func TestHeader_RequestRoundTrip(t *testing.T) {
var hdr Header var hdr Header
hdr.Reset(make([]byte, 0, 256)) hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
hdr.SetProtocol("HTTP/1.1") hdr.SetProtocol("HTTP/1.1")
hdr.SetMethod("POST") hdr.SetMethod("POST")
hdr.SetRequestTarget("/api/data") hdr.SetRequestTarget("/api/data")