Skip to main content

grpc_core/
status.rs

1//! Canonical gRPC status codes and their HTTP/2 trailer representation.
2
3/// The seventeen canonical gRPC status codes.
4#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5#[repr(i32)]
6pub enum Code {
7    Ok = 0,
8    Cancelled = 1,
9    Unknown = 2,
10    InvalidArgument = 3,
11    DeadlineExceeded = 4,
12    NotFound = 5,
13    AlreadyExists = 6,
14    PermissionDenied = 7,
15    ResourceExhausted = 8,
16    FailedPrecondition = 9,
17    Aborted = 10,
18    OutOfRange = 11,
19    Unimplemented = 12,
20    Internal = 13,
21    Unavailable = 14,
22    DataLoss = 15,
23    Unauthenticated = 16,
24}
25
26impl Code {
27    /// Returns the canonical code, treating unrecognized values as `Unknown`.
28    #[must_use]
29    pub const fn from_i32(value: i32) -> Self {
30        match value {
31            0 => Self::Ok,
32            1 => Self::Cancelled,
33            3 => Self::InvalidArgument,
34            4 => Self::DeadlineExceeded,
35            5 => Self::NotFound,
36            6 => Self::AlreadyExists,
37            7 => Self::PermissionDenied,
38            8 => Self::ResourceExhausted,
39            9 => Self::FailedPrecondition,
40            10 => Self::Aborted,
41            11 => Self::OutOfRange,
42            12 => Self::Unimplemented,
43            13 => Self::Internal,
44            14 => Self::Unavailable,
45            15 => Self::DataLoss,
46            16 => Self::Unauthenticated,
47            _ => Self::Unknown,
48        }
49    }
50
51    #[must_use]
52    pub const fn as_i32(self) -> i32 {
53        self as i32
54    }
55
56    /// Maps an HTTP status when the response has no `grpc-status` trailer.
57    ///
58    /// Even HTTP 200 maps to `Unknown`: HTTP success alone cannot establish
59    /// successful completion of a gRPC call.
60    #[must_use]
61    pub const fn from_http_status(status: u16) -> Self {
62        match status {
63            400 => Self::Internal,
64            401 => Self::Unauthenticated,
65            403 => Self::PermissionDenied,
66            404 => Self::Unimplemented,
67            429 | 502 | 503 | 504 => Self::Unavailable,
68            _ => Self::Unknown,
69        }
70    }
71}
72
73/// A gRPC outcome containing a canonical code and a UTF-8 diagnostic message.
74#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
75#[error("{code:?}: {message}")]
76pub struct Status {
77    code: Code,
78    message: String,
79}
80
81macro_rules! constructors {
82    ($($name:ident => $code:ident),+ $(,)?) => {
83        $(
84            #[must_use]
85            pub fn $name(message: impl Into<String>) -> Self {
86                Self::new(Code::$code, message)
87            }
88        )+
89    };
90}
91
92impl Status {
93    #[must_use]
94    pub fn new(code: Code, message: impl Into<String>) -> Self {
95        Self {
96            code,
97            message: message.into(),
98        }
99    }
100
101    constructors! {
102        ok => Ok,
103        cancelled => Cancelled,
104        unknown => Unknown,
105        invalid_argument => InvalidArgument,
106        deadline_exceeded => DeadlineExceeded,
107        not_found => NotFound,
108        already_exists => AlreadyExists,
109        permission_denied => PermissionDenied,
110        resource_exhausted => ResourceExhausted,
111        failed_precondition => FailedPrecondition,
112        aborted => Aborted,
113        out_of_range => OutOfRange,
114        unimplemented => Unimplemented,
115        internal => Internal,
116        unavailable => Unavailable,
117        data_loss => DataLoss,
118        unauthenticated => Unauthenticated,
119    }
120
121    #[must_use]
122    pub const fn code(&self) -> Code {
123        self.code
124    }
125
126    #[must_use]
127    pub fn message(&self) -> &str {
128        &self.message
129    }
130
131    /// Constructs a fallback outcome only when `grpc-status` is absent.
132    #[must_use]
133    pub fn from_http_status(status: u16) -> Self {
134        Self::new(
135            Code::from_http_status(status),
136            format!("HTTP status {status} without grpc-status"),
137        )
138    }
139
140    /// Encodes the message for a `grpc-message` trailer.
141    #[must_use]
142    pub fn encoded_message(&self) -> String {
143        encode_grpc_message(&self.message)
144    }
145}
146
147/// Percent-encodes UTF-8 bytes, leaving only RFC 3986 unreserved bytes literal.
148#[must_use]
149pub fn encode_grpc_message(message: &str) -> String {
150    const HEX: &[u8; 16] = b"0123456789ABCDEF";
151    let mut encoded = String::with_capacity(message.len());
152    for byte in message.bytes() {
153        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
154            encoded.push(char::from(byte));
155        } else {
156            encoded.push('%');
157            encoded.push(char::from(HEX[usize::from(byte >> 4)]));
158            encoded.push(char::from(HEX[usize::from(byte & 0xf)]));
159        }
160    }
161    encoded
162}
163
164/// Decodes a `grpc-message` without discarding malformed diagnostics.
165///
166/// Malformed percent escapes remain literal and invalid decoded UTF-8 becomes
167/// the replacement character. A literal plus sign remains a plus sign.
168#[must_use]
169pub fn decode_grpc_message(message: &str) -> String {
170    let input = message.as_bytes();
171    let mut decoded = Vec::with_capacity(input.len());
172    let mut offset = 0;
173    while offset < input.len() {
174        if input[offset] == b'%'
175            && let Some(escape) = input.get(offset + 1..offset + 3)
176            && let (Some(high), Some(low)) = (hex_value(escape[0]), hex_value(escape[1]))
177        {
178            decoded.push((high << 4) | low);
179            offset += 3;
180        } else {
181            decoded.push(input[offset]);
182            offset += 1;
183        }
184    }
185    String::from_utf8_lossy(&decoded).into_owned()
186}
187
188const fn hex_value(byte: u8) -> Option<u8> {
189    match byte {
190        b'0'..=b'9' => Some(byte - b'0'),
191        b'a'..=b'f' => Some(byte - b'a' + 10),
192        b'A'..=b'F' => Some(byte - b'A' + 10),
193        _ => None,
194    }
195}
196
197#[cfg(test)]
198mod unit {
199    use super::{Code, Status, decode_grpc_message, encode_grpc_message};
200
201    #[test]
202    fn canonical_codes_cover_all_seventeen_wire_values() {
203        for value in 0..=16 {
204            assert_eq!(Code::from_i32(value).as_i32(), value);
205        }
206        assert_eq!(Code::from_i32(-1), Code::Unknown);
207        assert_eq!(Code::from_i32(17), Code::Unknown);
208    }
209
210    #[test]
211    fn fallback_requires_grpc_status_even_for_http_success() {
212        for (http, expected) in [
213            (400, Code::Internal),
214            (401, Code::Unauthenticated),
215            (403, Code::PermissionDenied),
216            (404, Code::Unimplemented),
217            (429, Code::Unavailable),
218            (502, Code::Unavailable),
219            (503, Code::Unavailable),
220            (504, Code::Unavailable),
221            (200, Code::Unknown),
222            (500, Code::Unknown),
223        ] {
224            assert_eq!(Status::from_http_status(http).code(), expected);
225        }
226    }
227
228    #[test]
229    fn grpc_message_encodes_reserved_bytes_and_utf8() {
230        let message = "unreserved-._~ /%+\r\n\0\u{e9}";
231        let encoded = encode_grpc_message(message);
232        assert_eq!(encoded, "unreserved-._~%20%2F%25%2B%0D%0A%00%C3%A9");
233        assert_eq!(decode_grpc_message(&encoded), message);
234        assert_eq!(decode_grpc_message("%2f+%7e"), "/+~");
235        assert_eq!(decode_grpc_message(""), "");
236        assert_eq!(decode_grpc_message("plain text"), "plain text");
237    }
238
239    #[test]
240    fn malformed_escapes_are_preserved_and_invalid_utf8_is_replaced() {
241        for input in ["%", "%0", "%GG", "%0g"] {
242            assert_eq!(decode_grpc_message(input), input);
243        }
244        assert_eq!(decode_grpc_message("%FF"), "\u{fffd}");
245        assert_eq!(decode_grpc_message("bad%GG%20message"), "bad%GG message");
246        assert_eq!(decode_grpc_message("%C0%AF"), "\u{fffd}\u{fffd}");
247    }
248
249    #[test]
250    fn status_preserves_code_and_message_for_wire_encoding() {
251        let status = Status::internal(String::from("connection lost"));
252        assert_eq!(status.code(), Code::Internal);
253        assert_eq!(status.message(), "connection lost");
254        assert_eq!(status.encoded_message(), "connection%20lost");
255        assert_eq!(status.to_string(), "Internal: connection lost");
256    }
257}