grpc_webnext_client/
status.rs1use std::collections::HashMap;
4use std::fmt;
5
6use crate::metadata::Metadata;
7
8#[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 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#[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 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 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 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
110fn 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 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}