Under RESP3, GET <missing key> replies $-1 (the RESP2 null bulk) instead of
_. Redis replies _. The inline GET fast path writes the null as a hardcoded
byte string, so it never reaches the serializer that knows the connection's
protocol version.
src/server/conn/blocking.rs:1831:
None => {
write_buf.extend_from_slice(b"$-1\r\n");
crate::admin::metrics_setup::record_keyspace_miss();
It is per-KEY, not per-server
At --shards 4 the same command on the same RESP3 connection gives different
answers depending on which shard the key hashes to — keys the connection's own
shard owns take the inline path and get $-1; keys that route elsewhere come
back through the serializer and get _:
HELLO 3
GET m-a (absent) -> b'_\r\n' <-- remote shard, correct
GET m-b (absent) -> b'$-1\r\n' <-- local shard, wrong
GET m-c (absent) -> b'$-1\r\n'
GET m-d (absent) -> b'_\r\n'
GET m-e (absent) -> b'_\r\n'
GET m-f (absent) -> b'$-1\r\n'
GET m-g (absent) -> b'_\r\n'
GET m-h (absent) -> b'$-1\r\n'
At --shards 1 every key is local, so it is wrong 8/8.
A client cannot cache "this server speaks RESP3 correctly" — the answer changes
with the key. Any RESP3 decoder that treats an unexpected type as a protocol
error will fail intermittently and unreproducibly.
Every sibling command is correct
Same connection, same server, same absent-key shape:
HGET absent -> _
ZSCORE absent -> _
LINDEX absent -> _
GETDEL absent -> _
Only GET, and only on its inline path.
Redis reference
redis-server 8.6.1, HELLO 3, GET <absent> -> _\r\n.
This is #462 with a concrete instance
#462 asks to make bypassing the reply-conversion choke point structurally
impossible. This is that bypass, caught in the wild — and it is the second time
the inline GET path has diverged from the dispatched path (the first was the
inline-GET ACL bypass, #457). A fix that only patches line 1831 leaves the class
open; the inline path needs the protocol version, or it needs to stop writing
reply bytes itself.
Scope
- inline GET miss must emit
_ on a RESP3 connection and $-1 on RESP2
src/io/static_responses.rs:16 defines NULL_BULK with no callers — either
route the inline path through it with a RESP3 sibling, or delete it
- a test that asserts the reply for BOTH protocol versions AND for a local vs a
remote key at --shards >= 2, because a single-shard test cannot see the split
- sweep the inline path for other hardcoded reply bytes with the same blindness
Provenance
Found by rna2_resp3_collapses_both_nulls_to_underscore while building the
red suite for #482 — the test asserted GET under RESP3 as a contrast case
to the null array and the contrast failed. Not a null-array bug; filed
separately so #482 stays one contract.
Under RESP3,
GET <missing key>replies$-1(the RESP2 null bulk) instead of_. Redis replies_. The inline GET fast path writes the null as a hardcodedbyte string, so it never reaches the serializer that knows the connection's
protocol version.
src/server/conn/blocking.rs:1831:It is per-KEY, not per-server
At
--shards 4the same command on the same RESP3 connection gives differentanswers depending on which shard the key hashes to — keys the connection's own
shard owns take the inline path and get
$-1; keys that route elsewhere comeback through the serializer and get
_:At
--shards 1every key is local, so it is wrong 8/8.A client cannot cache "this server speaks RESP3 correctly" — the answer changes
with the key. Any RESP3 decoder that treats an unexpected type as a protocol
error will fail intermittently and unreproducibly.
Every sibling command is correct
Same connection, same server, same absent-key shape:
Only
GET, and only on its inline path.Redis reference
redis-server 8.6.1,HELLO 3,GET <absent>->_\r\n.This is #462 with a concrete instance
#462 asks to make bypassing the reply-conversion choke point structurally
impossible. This is that bypass, caught in the wild — and it is the second time
the inline GET path has diverged from the dispatched path (the first was the
inline-GET ACL bypass, #457). A fix that only patches line 1831 leaves the class
open; the inline path needs the protocol version, or it needs to stop writing
reply bytes itself.
Scope
_on a RESP3 connection and$-1on RESP2src/io/static_responses.rs:16definesNULL_BULKwith no callers — eitherroute the inline path through it with a RESP3 sibling, or delete it
remote key at
--shards >= 2, because a single-shard test cannot see the splitProvenance
Found by
rna2_resp3_collapses_both_nulls_to_underscorewhile building thered suite for #482 — the test asserted
GETunder RESP3 as a contrast caseto the null array and the contrast failed. Not a null-array bug; filed
separately so #482 stays one contract.