resp

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 23, 2026 License: BSD-3-Clause Imports: 6 Imported by: 0

Documentation

Overview

Package resp implements the Redis Serialization Protocol in both its RESP2 and RESP3 variants. It is a pure codec: it transforms byte buffers and has no dependency on net or on any other aki package, so the encoder and decoder can be fuzzed and unit-tested against in-memory buffers without a socket. The networking layer drives this package over a real connection.

The byte-level contract is doc 06 of the aki specification. Every framing decision here is made to be byte-identical to Redis so a client written for Redis talks to aki unchanged.

Index

Constants

View Source
const (
	// MaxMultibulkLen is the largest element count accepted in a client
	// multibulk request (1M).
	MaxMultibulkLen = 1024 * 1024
	// DefaultMaxBulkLen is the default proto-max-bulk-len (512 MiB): the largest
	// single bulk argument accepted.
	DefaultMaxBulkLen = 512 * 1024 * 1024
	// MaxInlineLen is the largest inline request line, including the newline
	// (64 KiB).
	MaxInlineLen = 64 * 1024
)

Protocol limits from doc 06 §6.4. The multibulk and inline caps are fixed; the bulk and query-buffer caps are defaults the networking layer may override from configuration.

Variables

View Source
var (
	ReplyOK         = []byte("+OK\r\n")
	ReplyPong       = []byte("+PONG\r\n")
	ReplyQueued     = []byte("+QUEUED\r\n")
	ReplyNil2       = []byte("$-1\r\n")
	ReplyNil3       = []byte("_\r\n")
	ReplyNilArray2  = []byte("*-1\r\n")
	ReplyEmptyArray = []byte("*0\r\n")
	ReplyEmptyMap3  = []byte("%0\r\n")
	ReplyEmptySet3  = []byte("~0\r\n")
	ReplyZero       = []byte(":0\r\n")
	ReplyOne        = []byte(":1\r\n")
	ReplyFalse3     = []byte("#f\r\n")
	ReplyTrue3      = []byte("#t\r\n")
	ReplyReset      = []byte("+RESET\r\n")

	// ReplyMaxClients is written to a connection that is accepted only to be
	// rejected because the server is at its maxclients limit (doc 19 §1.4).
	ReplyMaxClients = []byte("-ERR max number of clients reached\r\n")
)

Pre-built replies for the most common cases, kept as package-level byte slices so the hot path can write them with a single copy and no per-request allocation (doc 06 §11.6). The networking layer writes these directly into a client's output buffer; callers must never mutate the returned slices.

View Source
var ErrNeedMore = errors.New("need more data")

ErrNeedMore signals that the buffer does not yet hold a complete value. The decoder never advances its position when returning ErrNeedMore, so the caller can read more bytes from the socket and retry from the same offset.

Functions

func FormatDouble

func FormatDouble(f float64) string

FormatDouble renders a float for the wire: the special values become inf, -inf, nan, and finite values use the shortest representation that round-trips through float64 (doc 06 §11.5).

func ParseInline

func ParseInline(buf []byte, pos int) ([][]byte, int, error)

ParseInline parses one inline command line from buf starting at pos: the telnet-friendly form where a human types space-separated words instead of a multibulk frame (doc 06 §5). It returns the argument vector, the position past the terminating newline, and an error. ErrNeedMore means no complete line is buffered yet. A ProtocolError ("too big inline request" or "unbalanced quotes in request") is fatal and the connection is closed.

The parser strips a trailing CR (so both \n and \r\n terminate), splits on runs of whitespace, and honours single and double quoting with the same escape rules redis-cli uses.

func ParseRequest

func ParseRequest(buf []byte, pos int, maxBulkLen int64) ([][]byte, int, error)

ParseRequest extracts one client command from a query buffer. Client commands arrive either as a multibulk frame (every real client library) or as an inline line (a human on telnet); the first non-blank byte selects the path: '*' means multibulk, anything else means inline (doc 06 §5.5, §6).

It returns the argument vector (argv[0] is the command name), the position just past the consumed bytes, and an error. ErrNeedMore means the buffer does not yet hold a complete command and pos is returned unchanged, so the read loop can append more bytes and call again from the same offset. A ProtocolError is fatal: the read loop sends it and closes the connection.

A blank line (a lone CRLF or LF, which telnet clients send) is consumed and reported as an empty argv with a nil error; the caller skips it and retries.

maxBulkLen caps a single bulk argument (proto-max-bulk-len); pass DefaultMaxBulkLen for the default 512 MiB.

Types

type Encoder

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

Encoder serializes in-memory reply values to RESP. It carries the connection's negotiated protocol version (2 or 3) and chooses the wire shape accordingly: the command layer builds the same logical reply either way and the encoder is the single place the version is observed (doc 06 §4.5).

func NewEncoder

func NewEncoder(w Writer, proto int) *Encoder

NewEncoder returns an Encoder writing to w in the given protocol version. proto must be 2 or 3; any other value is treated as 2.

func (*Encoder) BeginStreamedBulkString

func (e *Encoder) BeginStreamedBulkString()

BeginStreamedBulkString opens a chunked bulk string whose total length is not known in advance ($?). Follow with WriteChunk calls and close with EndStreamedBulkString.

func (*Encoder) EndStreamedBulkString

func (e *Encoder) EndStreamedBulkString()

EndStreamedBulkString closes a streamed bulk string with the zero-length terminator chunk.

func (*Encoder) Proto

func (e *Encoder) Proto() int

Proto reports the protocol version this encoder emits (2 or 3).

func (*Encoder) SetProto

func (e *Encoder) SetProto(proto int)

SetProto switches the encoder to a new protocol version, as HELLO does mid-connection. proto other than 3 is normalized to 2.

func (*Encoder) WriteArrayLen

func (e *Encoder) WriteArrayLen(n int)

WriteArrayLen writes an array header. The caller then writes exactly n elements. Arrays are identical in RESP2 and RESP3.

func (*Encoder) WriteAttributeLen

func (e *Encoder) WriteAttributeLen(n int)

WriteAttributeLen writes an attribute (metadata) header for n pairs. The caller then writes n key-value pairs followed by the actual reply. In RESP2 attributes have no representation, so nothing is written and the caller's following reply stands alone.

func (*Encoder) WriteBigNumber

func (e *Encoder) WriteBigNumber(n *big.Int)

WriteBigNumber writes an arbitrary-precision integer. RESP3 uses the ( type; RESP2 downgrades to a bulk string of the decimal digits.

func (*Encoder) WriteBool

func (e *Encoder) WriteBool(b bool)

WriteBool writes a boolean. RESP3 uses the dedicated #t / #f type; RESP2 downgrades to the integers 1 and 0, matching how Redis reports EXPIRE, SISMEMBER, and the like.

func (*Encoder) WriteBulkError

func (e *Encoder) WriteBulkError(errStr string)

WriteBulkError writes an error that may contain newlines or exceed the inline limit. RESP3 uses the length-prefixed ! type for such errors; otherwise it falls back to a simple error.

func (*Encoder) WriteBulkString

func (e *Encoder) WriteBulkString(data []byte)

WriteBulkString writes a binary-safe bulk string ($). Valid and identical in both protocol versions.

func (*Encoder) WriteBulkStringStr

func (e *Encoder) WriteBulkStringStr(s string)

WriteBulkStringStr is WriteBulkString for a Go string, avoiding a []byte conversion on the caller's side.

func (*Encoder) WriteChunk

func (e *Encoder) WriteChunk(data []byte)

WriteChunk writes one chunk of a streamed bulk string.

func (*Encoder) WriteDouble

func (e *Encoder) WriteDouble(f float64)

WriteDouble writes a floating-point value. RESP3 uses the , type; RESP2 downgrades to a bulk string carrying the same decimal text, matching ZSCORE.

func (*Encoder) WriteError

func (e *Encoder) WriteError(errStr string)

WriteError writes a simple error (-). The string is "PREFIX message" and must not contain CR or LF. Simple errors are used in both RESP2 and RESP3 for the ordinary error path.

func (*Encoder) WriteInteger

func (e *Encoder) WriteInteger(n int64)

WriteInteger writes an integer (:). Identical in RESP2 and RESP3. Small values come from the pre-rendered pool.

func (*Encoder) WriteMapLen

func (e *Encoder) WriteMapLen(n int)

WriteMapLen writes a map header for n key-value pairs. RESP3 uses the % type; RESP2 downgrades to a flat array of 2n elements, the shape HGETALL has always had on RESP2.

func (*Encoder) WriteNull

func (e *Encoder) WriteNull()

WriteNull writes a nil reply. RESP3 collapses null bulk string and null array into the single _ type; RESP2 uses the null bulk string $-1.

func (*Encoder) WriteNullArray

func (e *Encoder) WriteNullArray()

WriteNullArray writes a nil aggregate reply. RESP3 uses _; RESP2 uses the null array *-1 (e.g. a BLPOP timeout).

func (*Encoder) WritePushLen

func (e *Encoder) WritePushLen(n int)

WritePushLen writes an out-of-band push header for n elements. RESP3 uses the > type; RESP2 downgrades to a plain array, which is how pub/sub messages have always been delivered on RESP2.

func (*Encoder) WriteRaw

func (e *Encoder) WriteRaw(p []byte)

WriteRaw writes pre-framed bytes verbatim, the path used for the pooled static replies. The bytes must already be a complete, correctly framed RESP value.

func (*Encoder) WriteSetLen

func (e *Encoder) WriteSetLen(n int)

WriteSetLen writes a set header for n elements. RESP3 uses the ~ type; RESP2 downgrades to a plain array.

func (*Encoder) WriteStatus

func (e *Encoder) WriteStatus(s string)

WriteStatus writes a simple string (+). Simple strings are identical in RESP2 and RESP3. s must not contain CR or LF.

func (*Encoder) WriteVerbatimString

func (e *Encoder) WriteVerbatimString(enc string, data []byte)

WriteVerbatimString writes a verbatim string with a 3-byte content-type hint (e.g. "txt", "mkd"). RESP3 uses the = type; RESP2 downgrades to a plain bulk string, dropping the hint. enc must be exactly three bytes.

type ProtocolError

type ProtocolError struct{ Msg string }

ProtocolError is a fatal decode error: the bytes are not valid RESP and the connection must be closed after the error string is sent to the client. Its Error string is already prefixed so it can be written straight to the wire as a RESP simple error.

func ErrProtocol

func ErrProtocol(msg string) ProtocolError

ErrProtocol builds a ProtocolError with the given description.

func (ProtocolError) Error

func (e ProtocolError) Error() string

type RESPType

type RESPType int

RESPType identifies a decoded value by its RESP leading byte. The constant values are the leading bytes themselves, so a decoder can switch on the byte read from the wire and a debugger shows a readable character.

const (
	// RESP2 base types, valid on every connection regardless of version.
	TypeSimpleString RESPType = '+'
	TypeError        RESPType = '-'
	TypeInteger      RESPType = ':'
	TypeBulkString   RESPType = '$'
	TypeArray        RESPType = '*'

	// RESP3 additions, sent only after a successful HELLO 3 but always accepted
	// from the wire for protocol symmetry.
	TypeNull      RESPType = '_'
	TypeBool      RESPType = '#'
	TypeDouble    RESPType = ','
	TypeBigNumber RESPType = '('
	TypeBulkError RESPType = '!'
	TypeVerbatim  RESPType = '='
	TypeMap       RESPType = '%'
	TypeSet       RESPType = '~'
	TypeAttribute RESPType = '|'
	TypePush      RESPType = '>'
)

type RESPValue

type RESPValue struct {
	Type     RESPType
	Str      []byte      // simple string, bulk string, verbatim string payload
	Integer  int64       // integer
	Float    float64     // double
	BigInt   *big.Int    // big number
	Bool     bool        // boolean
	Err      string      // simple error or bulk error message
	VerbEnc  string      // 3-char encoding prefix of a verbatim string
	Elems    []RESPValue // array, set, push elements
	Map      [][2]RESPValue
	Attrs    [][2]RESPValue // attribute metadata pairs (Type == TypeAttribute)
	AttrBody *RESPValue     // the reply an attribute is attached to
	IsNull   bool           // null bulk string, null array, or RESP3 null
}

RESPValue is a tagged union of every value the decoder can produce. Only the fields relevant to Type carry meaning; the rest are zero. It mirrors the shape Redis clients expect and is the type the aki cli decodes server replies into.

func Decode

func Decode(buf []byte, pos int) (RESPValue, int, error)

Decode reads one complete RESP value from buf starting at pos and returns the value, the position just past it, and an error. The two error classes are load-bearing: ErrNeedMore means the buffer is incomplete and pos is returned unchanged so the caller can retry after reading more; a ProtocolError means the bytes are invalid and the connection must be closed. Decode handles all RESP2 and RESP3 types and is used both by the server to parse replies in tests and by the aki cli to decode server output (doc 06 §12).

type Writer

type Writer interface {
	WriteString(s string) (int, error)
	WriteByte(b byte) error
	Write(p []byte) (int, error)
}

Writer is the sink the encoder writes framed bytes into. The networking layer satisfies it with a client's reply buffer; tests satisfy it with a bytes.Buffer. Encoding methods do not return errors: they accumulate into the buffer, and the single flush to the socket is where a write error surfaces.

Jump to

Keyboard shortcuts

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