RESP Protocol
This chapter isolates the wire-format adapter that sits between sockets, command parsing, and response encoding.
File boundary
src/protocol.rs
Why this module is central
The rest of the server assumes that a parsed Protocol value exists.
Important consumers are:
Cmd::from(...), which turns a top-level RESP array into a typed commandProtocol::encode(...), which turns command results back into bytes- replication helpers, which construct small RESP messages through
from_vec(...)andok()
So src/protocol.rs is both the inbound parser and the outbound serializer.
Internal protocol model
The enum is intentionally small:
SimpleString(String)BulkString(String)NullArray(Vec<Protocol>)
There is no dedicated Error, Integer, or binary-safe blob variant.
That design choice keeps the implementation short, but it has visible consequences later:
- error replies are encoded as simple strings
- integer-like values are usually rendered as strings
- payload fidelity is limited by the string-centric representation
Parsing contract
Protocol::from(protocol: &str) is the top-level parser.
Its contract is:
- inspect the first byte
- dispatch to a suffix parser
- return
(Protocol, consumed_len)
The consumed length is not incidental. It is what lets array parsing recurse through a single input buffer without reparsing from the beginning.
Dispatch currently goes to:
parse_simple_string_sfx(...)parse_bulk_string_sfx(...)parse_array_sfx(...)
Unsupported prefixes fail immediately.
parse_simple_string_sfx(...)
This parser is the smallest in the file.
It:
- searches for the first
\r\n - takes the bytes before it
- returns
SimpleString(...) - reports the consumed length including the delimiter
There is no extra validation beyond delimiter discovery.
parse_bulk_string_sfx(...)
Bulk strings use a two-stage parse.
Stage 1:
- read the decimal length prefix before the first
\r\n
Stage 2:
- read the following payload up to the next
\r\n - verify that the actual string length matches the declared length
If the lengths do not match, parsing fails instead of attempting recovery.
Important current behavior: payload lowercasing
When a bulk string is accepted, the parser stores it as:
1
Protocol::BulkString(s.to_lowercase())
That is convenient for command matching because Cmd::from(...) can assume lowercase command tokens.
But it also changes semantics:
- command names become case-insensitive
- keys are lowercased
- values are lowercased
- replicated external command payloads are not byte-exact copies of the original wire input
For example, SET Foo Bar enters the command layer as lowercase tokens.
This is a real implementation shortcut, not just a presentation detail.
parse_array_sfx(...)
Array parsing is where the (Protocol, consumed_len) contract pays off.
The control flow is:
- parse the array length prefix
- move an
offsetcursor past the header - call
Protocol::from(&s[offset..])for each child - advance
offsetby the child parser’s consumed length - collect all children into
Protocol::Array
The function is structurally recursive, but it still relies on the outer input being available as one contiguous &str.
Construction helpers used by other modules
The file also exposes a few helper constructors:
from_vec(...)ok()err(...)write_on_slave_err()psync_on_slave_err()none()
These helpers are important because higher layers often need to build RESP replies without repeating manual array or string assembly.
Two of them are especially revealing:
from_vec(...)constructs RESP arrays out of bulk strings and does not lowercase inputserr(...)returnsSimpleString, not a dedicated RESP error type
So internal helper-built messages and externally parsed messages do not have exactly the same semantics.
decode()
decode() flattens a Protocol value into a plain string.
Mapping:
SimpleString-> inner stringBulkString-> inner stringNull->""Array-> child strings joined by spaces
This is heavily used by Cmd::from(...) to turn a parsed array into command tokens.
That is practical, but it is not a lossless structural view. Nested arrays become flattened space-joined text.
encode()
encode() performs the reverse mapping back to RESP text.
Current rules are:
SimpleString->+...\r\nBulkString->$len\r\npayload\r\nArray->*len\r\nplus encoded childrenNull->$-1\r\n
This single serializer is reused for:
- ordinary client replies
- replication handshake replies
- command propagation to replicas
End-to-end data flow
For a normal command, the wire-format path is:
1
2
3
4
5
6
socket text
-> Protocol::from
-> Cmd::from
-> command handler result as Protocol
-> Protocol::encode
-> socket write
For internally generated replication messages, the path is usually:
1
2
3
helper constructor such as Protocol::from_vec
-> Protocol::encode
-> socket write
That difference matters because helper-generated data bypasses the lowercase-on-parse behavior.
Error surface
Parsing errors return DBError.
Semantic command errors are usually represented later as Protocol::err("..."), which still encodes to a RESP simple string.
So there are really two layers of failure:
- parser/build failures as Rust errors
- command/runtime failures as string replies
The module does not model the full RESP error vocabulary separately.
Current implementation limits
- parser input is
&str, not raw bytes - the parser assumes a full frame is already available in memory
- bulk strings are lowercased on parse
decode()flattens arrays into command-friendly text instead of preserving structure- error replies use
SimpleString - the enum does not model integers or binary-safe blobs separately
The protocol layer is deliberately small, but it is still the hinge that explains several later behaviors that would otherwise look surprising in command execution and replication.