Skip to main content

grpc_webnext_client/
status.rs

1//! gRPC status: the canonical codes, and reading one off a response.
2
3use std::collections::HashMap;
4use std::fmt;
5
6use crate::metadata::Metadata;
7
8/// The canonical gRPC status codes (<https://grpc.io/docs/guides/status-codes/>).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10#[repr(u16)]
11pub enum Code {
12    Ok = 0,
13    Cancelled = 1,
14    Unknown = 2,
15    InvalidArgument = 3,
16    DeadlineExceeded = 4,
17    NotFound = 5,
18    AlreadyExists = 6,
19    PermissionDenied = 7,
20    ResourceExhausted = 8,
21    FailedPrecondition = 9,
22    Aborted = 10,
23    OutOfRange = 11,
24    Unimplemented = 12,
25    Internal = 13,
26    Unavailable = 14,
27    DataLoss = 15,
28    Unauthenticated = 16,
29}
30
31impl Code {
32    /// Map a wire value; anything outside 0..=16 is `Unknown`, per the gRPC spec's
33    /// instruction to treat unrecognized codes as UNKNOWN rather than failing.
34    pub fn from_i32(value: i32) -> Code {
35        match value {
36            0 => Code::Ok,
37            1 => Code::Cancelled,
38            2 => Code::Unknown,
39            3 => Code::InvalidArgument,
40            4 => Code::DeadlineExceeded,
41            5 => Code::NotFound,
42            6 => Code::AlreadyExists,
43            7 => Code::PermissionDenied,
44            8 => Code::ResourceExhausted,
45            9 => Code::FailedPrecondition,
46            10 => Code::Aborted,
47            11 => Code::OutOfRange,
48            12 => Code::Unimplemented,
49            13 => Code::Internal,
50            14 => Code::Unavailable,
51            15 => Code::DataLoss,
52            16 => Code::Unauthenticated,
53            _ => Code::Unknown,
54        }
55    }
56}
57
58/// A terminal gRPC status. Trailing metadata rides along, because a server that
59/// fails is often the one with the most to say about why.
60#[derive(Debug, Clone)]
61pub struct Status {
62    pub code: Code,
63    pub message: String,
64    pub metadata: Metadata,
65}
66
67impl Status {
68    pub fn new(code: Code, message: impl Into<String>) -> Status {
69        Status { code, message: message.into(), metadata: Metadata::new() }
70    }
71
72    /// A transport-level failure: the call could not be carried. gRPC's status for
73    /// that is UNAVAILABLE, never UNKNOWN — UNKNOWN means the *server* said
74    /// something we could not interpret.
75    pub fn unavailable(message: impl Into<String>) -> Status {
76        Status::new(Code::Unavailable, message)
77    }
78
79    pub fn is_ok(&self) -> bool {
80        self.code == Code::Ok
81    }
82
83    /// Read `grpc-status` / `grpc-message` out of a header or trailer block.
84    /// Returns `None` when there is no `grpc-status` at all, which is how the
85    /// caller tells "no trailers yet" from "trailers said OK".
86    pub fn from_headers(headers: &HashMap<String, String>) -> Option<Status> {
87        let raw = headers.get(GRPC_STATUS)?;
88        let code = raw.trim().parse::<i32>().map(Code::from_i32).unwrap_or(Code::Unknown);
89        let message = headers.get(GRPC_MESSAGE).map(|m| decode_grpc_message(m)).unwrap_or_default();
90        let mut metadata = Metadata::from_headers(headers);
91        // The status itself is not user metadata.
92        for key in [GRPC_STATUS, GRPC_MESSAGE] {
93            metadata.remove(key);
94        }
95        Some(Status { code, message, metadata })
96    }
97}
98
99impl fmt::Display for Status {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "{:?}: {}", self.code, self.message)
102    }
103}
104
105impl std::error::Error for Status {}
106
107pub(crate) const GRPC_STATUS: &str = "grpc-status";
108pub(crate) const GRPC_MESSAGE: &str = "grpc-message";
109
110/// `grpc-message` is percent-encoded on the wire (gRPC's own restricted variant).
111/// An undecodable value is passed through rather than lost — a mangled message is
112/// still more useful than none.
113fn decode_grpc_message(value: &str) -> String {
114    percent_encoding::percent_decode_str(value)
115        .decode_utf8()
116        .map(|s| s.into_owned())
117        .unwrap_or_else(|_| value.to_string())
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn headers(pairs: &[(&str, &str)]) -> HashMap<String, String> {
125        pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
126    }
127
128    #[test]
129    fn no_grpc_status_is_none() {
130        assert!(Status::from_headers(&headers(&[("content-type", "application/grpc")])).is_none());
131    }
132
133    #[test]
134    fn reads_code_and_percent_decoded_message() {
135        let s = Status::from_headers(&headers(&[
136            ("grpc-status", "9"),
137            ("grpc-message", "not%20ready%3A%20try%20later"),
138        ]))
139        .unwrap();
140        assert_eq!(s.code, Code::FailedPrecondition);
141        assert_eq!(s.message, "not ready: try later");
142    }
143
144    #[test]
145    fn keeps_trailing_metadata_but_not_the_status_itself() {
146        let s = Status::from_headers(&headers(&[
147            ("grpc-status", "0"),
148            ("grpc-message", "ok"),
149            ("x-trailer", "value"),
150        ]))
151        .unwrap();
152        assert_eq!(s.metadata.get("x-trailer"), Some("value"));
153        assert!(s.metadata.get("grpc-status").is_none());
154        assert!(s.metadata.get("grpc-message").is_none());
155    }
156
157    #[test]
158    fn unrecognized_codes_are_unknown_not_an_error() {
159        // The spec says treat an unrecognized code as UNKNOWN; failing to parse the
160        // response entirely would turn a live server into a dead one.
161        for raw in ["99", "-1", "banana", ""] {
162            let s = Status::from_headers(&headers(&[("grpc-status", raw)])).unwrap();
163            assert_eq!(s.code, Code::Unknown, "grpc-status: {raw:?}");
164        }
165    }
166
167    #[test]
168    fn undecodable_message_is_passed_through() {
169        let s = Status::from_headers(&headers(&[("grpc-status", "2"), ("grpc-message", "%ZZ")]))
170            .unwrap();
171        assert_eq!(s.message, "%ZZ");
172    }
173}