Skip to main content

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}