yo_resp/error.rs
1//! What the codec refuses, and the exact words it refuses it in.
2//!
3//! The messages are Redis's messages, character for character. A client that
4//! branches on the text of a protocol error is doing something questionable,
5//! but the differential harness in `yo-compat` compares replies byte for byte,
6//! and a protocol error is a reply. Anything invented here would be a
7//! divergence that has to be registered, so nothing is invented here.
8
9use core::fmt;
10use yo_common::{Code, Error};
11
12/// A frame the codec will not accept.
13///
14/// This is a value rather than a string because the connection has to do two
15/// things with it: write the Redis text to the client, and close the
16/// connection, which is what Redis does after any protocol error.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum ProtocolError {
20 /// The `*` count was not an integer, was negative past what Redis allows,
21 /// or was larger than the multibulk limit.
22 InvalidMultibulkLength,
23 /// The `$` length was not an integer, was negative, or was larger than the
24 /// bulk limit.
25 InvalidBulkLength,
26 /// An argument did not begin with `$`. Carries the byte that was there
27 /// instead, because that byte is in the message Redis sends.
28 ExpectedDollar(u8),
29 /// The `*` count line has not ended and there is already more pending than
30 /// an inline request is allowed to be.
31 TooBigMbulkCount,
32 /// The `$` length line has not ended and there is already more pending than
33 /// an inline request is allowed to be.
34 TooBigBulkCount,
35 /// An inline request has no newline and has passed the inline limit.
36 TooBigInline,
37 /// An inline request opened a quote it never closed.
38 UnbalancedQuotes,
39 /// A reply began with a byte that is not a type in either protocol.
40 /// Carries the byte.
41 UnknownType(u8),
42 /// A reply nested deeper than the configured limit. This is only reachable
43 /// from the reply decoder, which is the one part of the codec that recurses.
44 TooDeep,
45 /// A frame this build does not decode, currently the streamed aggregates
46 /// and streamed strings. Carries the type byte.
47 Unsupported(u8),
48}
49
50impl ProtocolError {
51 /// Appends the error line the client should see, `-` and CRLF included.
52 ///
53 /// Written straight into the output buffer rather than returned as a
54 /// `String`, because the failure path runs on a shard thread too and a
55 /// shard thread that allocates aborts.
56 pub fn write_reply(&self, out: &mut Vec<u8>) {
57 out.extend_from_slice(b"-ERR Protocol error: ");
58 match *self {
59 ProtocolError::InvalidMultibulkLength => {
60 out.extend_from_slice(b"invalid multibulk length");
61 }
62 ProtocolError::InvalidBulkLength => out.extend_from_slice(b"invalid bulk length"),
63 ProtocolError::ExpectedDollar(got) => {
64 out.extend_from_slice(b"expected '$', got '");
65 // Redis prints the offending byte with `%c` and then flattens
66 // newlines to spaces, because a newline inside an error line
67 // would end the line early and desynchronise the client.
68 out.push(if got == b'\r' || got == b'\n' {
69 b' '
70 } else {
71 got
72 });
73 out.push(b'\'');
74 }
75 ProtocolError::TooBigMbulkCount => {
76 out.extend_from_slice(b"too big mbulk count string");
77 }
78 ProtocolError::TooBigBulkCount => out.extend_from_slice(b"too big bulk count string"),
79 ProtocolError::TooBigInline => out.extend_from_slice(b"too big inline request"),
80 ProtocolError::UnbalancedQuotes => {
81 out.extend_from_slice(b"unbalanced quotes in request");
82 }
83 ProtocolError::UnknownType(got) => {
84 out.extend_from_slice(b"unknown type byte '");
85 out.push(if got == b'\r' || got == b'\n' {
86 b' '
87 } else {
88 got
89 });
90 out.push(b'\'');
91 }
92 ProtocolError::TooDeep => out.extend_from_slice(b"nesting too deep"),
93 ProtocolError::Unsupported(got) => {
94 out.extend_from_slice(b"unsupported type byte '");
95 out.push(if got == b'\r' || got == b'\n' {
96 b' '
97 } else {
98 got
99 });
100 out.push(b'\'');
101 }
102 }
103 out.extend_from_slice(b"\r\n");
104 }
105}
106
107impl fmt::Display for ProtocolError {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 let mut line = Vec::new();
110 self.write_reply(&mut line);
111 // Strip the `-ERR ` that belongs to the wire and the trailing CRLF that
112 // belongs to the frame. What is left is the sentence.
113 let body = &line[5..line.len() - 2];
114 f.write_str(&String::from_utf8_lossy(body))
115 }
116}
117
118impl core::error::Error for ProtocolError {}
119
120impl From<ProtocolError> for Error {
121 /// A protocol error reaching the typed API is an invalid argument, because
122 /// on that side of the boundary the caller handed us the bytes.
123 fn from(e: ProtocolError) -> Error {
124 Error::new(Code::Invalid, e.to_string())
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn reply(e: ProtocolError) -> String {
133 let mut v = Vec::new();
134 e.write_reply(&mut v);
135 String::from_utf8(v).unwrap()
136 }
137
138 #[test]
139 fn the_messages_are_the_ones_redis_sends() {
140 assert_eq!(
141 reply(ProtocolError::InvalidMultibulkLength),
142 "-ERR Protocol error: invalid multibulk length\r\n"
143 );
144 assert_eq!(
145 reply(ProtocolError::InvalidBulkLength),
146 "-ERR Protocol error: invalid bulk length\r\n"
147 );
148 assert_eq!(
149 reply(ProtocolError::ExpectedDollar(b'x')),
150 "-ERR Protocol error: expected '$', got 'x'\r\n"
151 );
152 assert_eq!(
153 reply(ProtocolError::TooBigMbulkCount),
154 "-ERR Protocol error: too big mbulk count string\r\n"
155 );
156 assert_eq!(
157 reply(ProtocolError::TooBigBulkCount),
158 "-ERR Protocol error: too big bulk count string\r\n"
159 );
160 assert_eq!(
161 reply(ProtocolError::TooBigInline),
162 "-ERR Protocol error: too big inline request\r\n"
163 );
164 assert_eq!(
165 reply(ProtocolError::UnbalancedQuotes),
166 "-ERR Protocol error: unbalanced quotes in request\r\n"
167 );
168 }
169
170 /// The offending byte is printed, and a newline in it must not end the
171 /// line, because a client reading a short line then reads the rest of the
172 /// error as its next reply and every reply after that is off by one.
173 #[test]
174 fn a_newline_in_the_offending_byte_does_not_end_the_line() {
175 let r = reply(ProtocolError::ExpectedDollar(b'\n'));
176 assert_eq!(r, "-ERR Protocol error: expected '$', got ' '\r\n");
177 assert_eq!(r.matches("\r\n").count(), 1);
178 }
179
180 #[test]
181 fn it_carries_into_the_typed_api_as_an_invalid_argument() {
182 let e: Error = ProtocolError::InvalidBulkLength.into();
183 assert_eq!(e.code(), Code::Invalid);
184 assert_eq!(e.message(), "Protocol error: invalid bulk length");
185 }
186}