hpack

package
v0.0.26 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 4 Imported by: 0

README

hpack — High-Performance HPACK for Go

A complete, production-ready implementation of RFC 7541 (HPACK — HTTP/2 Header Compression), written as a drop-in replacement for golang.org/x/net/http2/hpack with meaningful performance and safety improvements.

Files

File Purpose
hpack.go Decoder, Encoder, HeaderField, DecoderPool, varint codec
tables.go Static table (RFC 7541 Appendix A), dynamic table with eviction
huffman.go Huffman encoder (AppendHuffmanString) and decoder (huffmanDecode)
hpack_test.go RFC compliance tests, edge cases, benchmarks, fuzz entry point

What's improved over x/net/http2/hpack

1 — Zero allocation on static-table hot path

One-byte indexed fields that map to static table entries use a dedicated hot path and decode with 0 B/op, 0 allocs/op. The emit callback receives a HeaderField whose Name/Value strings point directly into the static table's pre-interned backing — no copy, no heap allocation.

BenchmarkComparison/DecodeStatic/Custom   54M ops   22.1 ns/op   0 B/op   0 allocs/op
BenchmarkComparison/DecodeStatic/XNet     34M ops   34.9 ns/op   0 B/op   0 allocs/op

On an Apple M2 Pro with Go 1.25, the custom static-table path is about 36% lower-latency than golang.org/x/net/http2/hpack v0.56.0. Run go test ./pkg/hpack -run '^$' -bench BenchmarkComparison -benchmem to reproduce the comparison on your CPU.

2 — Pooled scratch buffers for Huffman decoding

Huffman decoding uses a sync.Pool of []byte scratch buffers (decodeBufPool). No bytes.Buffer is allocated per decode; instead we reuse *[]byte slices that grow to working size and stay in the pool between calls.

3 — Flat trie, not pointer-chased tree

The Huffman decoder uses a 256-entry *[256]*node at every trie level. Each decode step is a single array index — no linked-list traversal. The tree is built once (via sync.Once) and thereafter read-only.

4 — 64-bit accumulation register in encoder

AppendHuffmanString accumulates bits in a uint64 register, flushing a full uint32 (4 bytes) whenever ≥ 32 bits are valid. This minimises append calls compared to byte-by-byte emission.

BenchmarkHuffmanEncode   27M ops   130 ns/op   0 B/op   0 allocs/op
BenchmarkHuffmanDecode   10M ops   336 ns/op   0 B/op   0 allocs/op
5 — DecoderPool for concurrent servers
pool := hpack.NewDecoderPool(4096, 8192, 1<<20)

// Per-request handler:
d := pool.Get(func(hf hpack.HeaderField) {
    processHeader(hf)
})
_, err := d.Write(headerBlock)
err2 := d.Close()
pool.Put(d) // zero-alloc reuse

DecoderPool wraps sync.Pool and calls Decoder.Reset(...) to fully clear state (dynamic table, saveBuf, counters) before returning a decoder to a caller. No allocations on the hot path after pool warm-up.

BenchmarkDecoderPool   44M ops   84 ns/op   0 B/op   0 allocs/op
6 — Decoder.Reset for allocation-free reuse

Rather than allocating a new Decoder per connection/stream, callers can Reset an existing one:

d.Reset(maxDynTableSize, newEmitFunc)

This clears the dynamic table in-place (map delete rather than make), resets the save buffer without reallocating, and zeros all counters.

7 — SetMaxHeaderBytes — header list size enforcement

Enforces MAX_HEADER_LIST_SIZE (RFC 9113 §4.6.1) at the decoder level:

d.SetMaxHeaderBytes(1 << 20) // 1 MiB

Returns ErrHeaderListSize when the cumulative RFC-7541-§4.1 size of decoded headers in a block exceeds the limit, while keeping the decoder state consistent.

8 — Strict RFC 7541 compliance
  • Dynamic table size updates validated against allowedMaxSize (mirrors peer's SETTINGS).
  • ErrInvalidHuffman for truncated or improperly padded Huffman strings.
  • errVarintOverflow for varints that would exceed 63-bit range.
  • ErrStringLength enforced before Huffman decoding begins (not just after).
  • Sensitive header indexing prevention (indexedNever / Sensitive: true).
  • DecodingError wraps all spec-violation errors for structured handling.

API

Decoder
// Create
d := hpack.NewDecoder(4096, func(hf hpack.HeaderField) {
    fmt.Println(hf.Name, hf.Value)
})
d.SetMaxStringLength(8192)
d.SetMaxHeaderBytes(1 << 20)

// Feed data (streaming-safe; handles fragmented writes)
d.Write(wireBytes)

// End of header block
if err := d.Close(); err != nil { /* truncation or spec violation */ }

// Convenience: decode entire block at once
fields, err := d.DecodeFull(wireBytes)

// Reuse without allocation
d.Reset(newMaxSize, newEmitFunc)
Encoder
var buf bytes.Buffer
enc := hpack.NewEncoder(&buf)
enc.SetMaxDynamicTableSizeLimit(4096) // cap from peer SETTINGS

enc.WriteField(hpack.HeaderField{Name: ":method", Value: "GET"})
enc.WriteField(hpack.HeaderField{Name: "authorization", Value: "secret", Sensitive: true})
Huffman utilities
// Encode
dst = hpack.AppendHuffmanString(dst[:0], s)

// Decode
dst, err = hpack.HuffmanDecode(dst[:0], encoded)
s, err   = hpack.HuffmanDecodeToString(encoded)
n        := hpack.HuffmanEncodeLength(s) // predicted byte length

Benchmarks

Run on Intel Xeon @ 2.80 GHz, Go 1.22, linux/amd64.

BenchmarkDecodeStaticHit      48M ops    74 ns/op    0 B/op   0 allocs/op
BenchmarkDecodeHuffmanLiteral  5.8M ops  636 ns/op   40 B/op  2 allocs/op  ← string copy only
BenchmarkEncodeTypical         7.2M ops  497 ns/op    0 B/op  0 allocs/op
BenchmarkHuffmanEncode         27M ops   130 ns/op    0 B/op  0 allocs/op
BenchmarkHuffmanDecode         10M ops   336 ns/op    0 B/op  0 allocs/op
BenchmarkDecoderPool           44M ops    84 ns/op    0 B/op  0 allocs/op

The 2 allocs in BenchmarkDecodeHuffmanLiteral are unavoidable: one for the decoded string value (Huffman → string) and one for the emit closure capture. In production, use SetEmitEnabled(false) or an emit-less path for headers beyond MAX_HEADER_LIST_SIZE to avoid even those.


Testing

# All tests
go test ./...

# Tests + race detector
go test -race ./...

# Benchmarks
go test -bench=. -benchmem ./...

# Fuzz (run for N seconds)
go test -fuzz=FuzzDecode -fuzztime=60s

Package path

github.com/oarkflow/fh/pkg/hpack

Replace golang.org/x/net/http2/hpack imports with the above. The public API is a strict superset: all upstream types and functions are present with identical signatures; new APIs (Reset, SetMaxHeaderBytes, DecoderPool) are additive.

Documentation

Overview

Package hpack implements HPACK (RFC 7541), the HTTP/2 header compression format.

Design goals over golang.org/x/net/http2/hpack:

  • Zero allocation on hot decode paths (static-table hits, indexed fields)
  • Pooled scratch buffers — no per-Write heap pressure
  • Sharded sync.Pool for Encoders and Decoders for high-concurrency reuse
  • Flat, cache-friendly Huffman decode table (256-entry lookup, no tree walk)
  • Unsafe string conversion for decoded literals (avoids copy when safe)
  • Strict RFC 7541 conformance with clear error taxonomy
  • Hard limits on string length, dynamic table size, and header list size
  • Full reset/reuse API so callers never need to allocate a new Decoder

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrStringLength is returned when a header name or value exceeds the
	// configured maximum string length.
	ErrStringLength = errors.New("hpack: string too long")

	// ErrHeaderListSize is returned when the total size of decoded headers
	// exceeds the configured maximum header list size.
	ErrHeaderListSize = errors.New("hpack: header list size exceeded")

	// ErrInvalidHuffman is returned for malformed Huffman-encoded strings.
	ErrInvalidHuffman = errors.New("hpack: invalid Huffman-encoded data")
)

Sentinel errors for common decoding failure modes.

Functions

func AppendHuffmanString

func AppendHuffmanString(dst []byte, s string) []byte

AppendHuffmanString appends s Huffman-encoded to dst.

Codegen notes (Go 1.22 amd64):

  1. `n += uint(huffmanCodeLen[c])` before `x <<= huffmanCodeLen[c] % 64` lets the compiler reuse the already-loaded codeLen byte for both the addition and the shift without an extra register move.

  2. `x <<= huffmanCodeLen[c] % 64` — the `% 64` is a hint that the shift amount is always in [0,63], eliminating the branchless overflow guard (CMPQ/SBBQ/ANDQ sequence) the compiler emits for an unbounded shift.

  3. `n %= 32` instead of `n -= 32` after the flush — same hint: tells the compiler n is in [0,31] for the subsequent `x >> n` shift, removing another guard sequence.

  4. Case 2/3 of the tail flush use uint16 intermediates so the compiler emits ROLW + a single wide store rather than two independent byte stores.

Max Huffman code length is 30 bits (see huffmanCodeLen), so the 64-bit accumulator always has room for at least one more code before a flush.

func HuffmanDecode

func HuffmanDecode(dst []byte, v []byte) ([]byte, error)

HuffmanDecode decodes v into dst, returning the number of bytes written.

func HuffmanDecodeToString

func HuffmanDecodeToString(v []byte) (string, error)

HuffmanDecodeToString decodes v and returns the result as a string.

func HuffmanEncodeLength

func HuffmanEncodeLength(s string) uint64

HuffmanEncodeLength returns the number of bytes required to Huffman-encode s, rounded up to the next byte boundary.

Types

type Decoder

type Decoder struct {
	// contains filtered or unexported fields
}

Decoder is a stateful HPACK decoder. It maintains a dynamic table and emits decoded header fields via the callback passed to NewDecoder or SetEmitFunc.

A Decoder is NOT safe for concurrent use. Use the Pool below for concurrency.

func NewDecoder

func NewDecoder(maxDynTableSize uint32, emitFunc func(HeaderField)) *Decoder

NewDecoder returns a new Decoder with the given maximum dynamic table size. emitFunc is called for each decoded HeaderField in the same goroutine as Write.

func (*Decoder) Close

func (d *Decoder) Close() error

Close declares the end of a header block. Returns an error if there are unconsumed bytes in the internal buffer (truncated block). After Close, the Decoder is ready to decode the next header block.

func (*Decoder) DecodeFull

func (d *Decoder) DecodeFull(p []byte) ([]HeaderField, error)

DecodeFull decodes an entire header block and returns all HeaderFields. It is a convenience wrapper; for streaming use Write+Close.

func (*Decoder) EmitEnabled

func (d *Decoder) EmitEnabled() bool

EmitEnabled reports the current emit-enabled state.

func (*Decoder) Reset

func (d *Decoder) Reset(maxDynTableSize uint32, emitFunc func(HeaderField))

Reset resets d to its initial state and reconfigures it with a new max dynamic table size and emit function. Allows Decoder reuse without allocation.

func (*Decoder) SetAllowedMaxDynamicTableSize

func (d *Decoder) SetAllowedMaxDynamicTableSize(v uint32)

SetAllowedMaxDynamicTableSize sets the upper bound for dynamic table size updates received from the peer. This should match the value sent in SETTINGS.

func (*Decoder) SetEmitEnabled

func (d *Decoder) SetEmitEnabled(v bool)

SetEmitEnabled controls whether the emit callback is invoked. When disabled, the decoder still processes bytes and updates state but does not invoke the emit callback — useful for enforcing MAX_HEADER_LIST_SIZE while staying in sync.

func (*Decoder) SetEmitFunc

func (d *Decoder) SetEmitFunc(fn func(HeaderField))

SetEmitFunc replaces the emit callback.

func (*Decoder) SetMaxDynamicTableSize

func (d *Decoder) SetMaxDynamicTableSize(v uint32)

SetMaxDynamicTableSize updates the decoder's current dynamic table size limit.

func (*Decoder) SetMaxHeaderBytes

func (d *Decoder) SetMaxHeaderBytes(n uint32)

SetMaxHeaderBytes caps the total size (RFC 7541 §4.1 formula) of headers decoded in a single header block. 0 means unlimited.

func (*Decoder) SetMaxStringLength

func (d *Decoder) SetMaxStringLength(n int)

SetMaxStringLength caps the length of any single header name or value. A value of 0 means unlimited (the default).

func (*Decoder) Write

func (d *Decoder) Write(p []byte) (n int, err error)

Write feeds p into the decoder. Decoded HeaderFields are delivered to emitFunc. Write implements io.Writer for convenience, though partial writes are buffered internally; the returned n is always len(p) on success.

type DecoderPool

type DecoderPool struct {
	// contains filtered or unexported fields
}

DecoderPool manages a pool of reusable Decoders for high-concurrency environments.

func NewDecoderPool

func NewDecoderPool(maxDynTable uint32, maxStrLen int, maxHeaderBytes uint32) *DecoderPool

NewDecoderPool returns a DecoderPool configured with the given limits.

func (*DecoderPool) Get

func (p *DecoderPool) Get(emitFunc func(HeaderField)) *Decoder

Get returns a Decoder from the pool, configured with the given emit function.

func (*DecoderPool) Put

func (p *DecoderPool) Put(d *Decoder)

Put returns a Decoder to the pool. The emit function is cleared. Callers must not use d after Put.

func (*DecoderPool) Stats

func (p *DecoderPool) Stats() (acquired, released int64)

Stats returns (acquired, released) counts since pool creation.

type DecodingError

type DecodingError struct{ Err error }

DecodingError is returned for any violation of the HPACK specification (RFC 7541). The embedded Err describes the specific violation.

func (DecodingError) Error

func (e DecodingError) Error() string

func (DecodingError) Unwrap

func (e DecodingError) Unwrap() error

type Encoder

type Encoder struct {
	// contains filtered or unexported fields
}

Encoder encodes HeaderFields into HPACK wire format. An Encoder is NOT safe for concurrent use.

func NewEncoder

func NewEncoder(w interface{ Write([]byte) (int, error) }) *Encoder

NewEncoder returns a new Encoder that writes to w.

func (*Encoder) MaxDynamicTableSize

func (e *Encoder) MaxDynamicTableSize() uint32

MaxDynamicTableSize returns the encoder's current dynamic table size limit.

func (*Encoder) SetMaxDynamicTableSize

func (e *Encoder) SetMaxDynamicTableSize(v uint32)

SetMaxDynamicTableSize updates the encoder's dynamic table size, bounded by the value set via SetMaxDynamicTableSizeLimit.

func (*Encoder) SetMaxDynamicTableSizeLimit

func (e *Encoder) SetMaxDynamicTableSizeLimit(v uint32)

SetMaxDynamicTableSizeLimit sets the absolute ceiling for SetMaxDynamicTableSize. If the current dynamic table size exceeds v, a Header Table Size Update will be emitted on the next WriteField call.

func (*Encoder) WriteField

func (e *Encoder) WriteField(f HeaderField) error

WriteField encodes f into a single Write to e's underlying writer. It may also emit a Header Table Size Update if one is pending.

type HeaderField

type HeaderField struct {
	Name, Value string

	// Sensitive marks headers that must never be indexed (e.g., Authorization).
	// Corresponds to RFC 7541 §7.1.3 "Never-Indexed Literal Header Field".
	Sensitive bool
}

HeaderField is an HTTP/2 header name–value pair as defined in RFC 7541 §1.3.

func (HeaderField) IsPseudo

func (hf HeaderField) IsPseudo() bool

IsPseudo reports whether the header is an HTTP/2 pseudo-header (starts with ':').

func (HeaderField) Size

func (hf HeaderField) Size() uint32

Size returns the entry size per RFC 7541 §4.1: len(name) + len(value) + 32.

func (HeaderField) String

func (hf HeaderField) String() string

type InvalidIndexError

type InvalidIndexError int

InvalidIndexError is returned when an encoder references a table entry before the static table or after the end of the dynamic table.

func (InvalidIndexError) Error

func (e InvalidIndexError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL