poem_grpc/status.rs
1use std::fmt::Display;
2
3use http::{HeaderMap, header::HeaderValue};
4use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, percent_encode};
5
6use crate::Metadata;
7
8const GRPC_STATUS_HEADER_CODE: &str = "grpc-status";
9const GRPC_STATUS_MESSAGE_HEADER: &str = "grpc-message";
10
11const ENCODING_SET: &AsciiSet = &CONTROLS
12 .add(b' ')
13 .add(b'"')
14 .add(b'#')
15 .add(b'<')
16 .add(b'>')
17 .add(b'`')
18 .add(b'?')
19 .add(b'{')
20 .add(b'}');
21
22/// GRPC status code
23///
24/// Reference: <https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc>
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum Code {
27 /// Not an error.
28 Ok,
29
30 /// The operation was cancelled, typically by the caller.
31 Cancelled,
32
33 /// Unknown error.
34 ///
35 /// For example, this error may be returned when a Status value received
36 /// from another address space belongs to an error space that is not known
37 /// in this address space. Also errors raised by APIs that do not return
38 /// enough error information may be converted to this error.
39 Unknown,
40
41 /// The client specified an invalid argument.
42 ///
43 /// Note that this differs from `FailedPrecondition`. `InvalidArgument`
44 /// indicates arguments that are problematic regardless of the state of the
45 /// system (e.g., a malformed file name).
46 InvalidArgument,
47
48 /// The deadline expired before the operation could complete.
49 ///
50 /// For operations that change the state of the system, this error may be
51 /// returned even if the operation has completed successfully. For
52 /// example, a successful response from a server could have been delayed
53 /// long.
54 DeadlineExceeded,
55
56 /// Some requested entity
57 ///
58 /// (e.g., file or directory) was not found. Note to server developers: if a
59 /// request is denied for an entire class of users, such as gradual feature
60 /// rollout or undocumented allowlist, `NotFound` may be used. If a request
61 /// is denied for some users within a class of users, such as user-based
62 /// access control, PermissionDenied must be used.
63 NotFound,
64
65 /// The entity that a client attempted to create (e.g., file or directory)
66 /// already exists.
67 AlreadyExists,
68
69 /// The caller does not have permission to execute the specified operation.
70 ///
71 /// `PermissionDenied` must not be used for rejections caused by exhausting
72 /// some resource (use `ResourceExhausted` instead for those errors).
73 /// PermissionDenied must not be used if the caller can not be identified
74 /// (use `Unauthenticated` instead for those errors). This error code does
75 /// not imply the request is valid or the requested entity exists or
76 /// satisfies other pre-conditions.
77 PermissionDenied,
78
79 /// Some resource has been exhausted, perhaps a per-user quota, or perhaps
80 /// the entire file system is out of space.
81 ResourceExhausted,
82
83 /// The operation was rejected because the system is not in a state required
84 /// for the operation's execution.
85 ///
86 /// For example, the directory to be deleted is non-empty, an rmdir
87 /// operation is applied to a non-directory, etc. Service implementors can
88 /// use the following guidelines to decide between `FailedPrecondition`,
89 /// `Aborted`, and `Unavailable`:
90 ///
91 /// (a) Use `Unavailable` if the client can retry just the failing call.
92 ///
93 /// (b) Use `Aborted` if the client should retry at a higher level
94 /// (e.g., when a client-specified test-and-set fails, indicating
95 /// the client should restart a read-modify-write sequence).
96 ///
97 /// (c) Use `FailedPrecondition` if the client should not retry until
98 /// the system state has been explicitly fixed. E.g., if an
99 /// "rmdir" fails because the directory is non-empty,
100 /// `FailedPrecondition` should be returned since the client
101 /// should not retry unless the files are deleted from the
102 /// directory.
103 FailedPrecondition,
104
105 /// The operation was aborted, typically due to a concurrency issue such as
106 /// a sequencer check failure or transaction abort. See the guidelines above
107 /// for deciding between `FailedPrecondition`, `Aborted`, and `Unavailable`.
108 Aborted,
109
110 /// The operation was attempted past the valid range. E.g., seeking or
111 /// reading past end-of-file.
112 ///
113 /// Unlike `InvalidArgument`, this error indicates
114 /// a problem that may be fixed if the system state changes. For example, a
115 /// 32-bit file system will generate `InvalidArgument` if asked to read at
116 /// an offset that is not in the range [0,2^32-1], but it will generate
117 /// `OutOfRange` if asked to read from an offset past the current file size.
118 /// There is a fair bit of overlap between `FailedPrecondition` and
119 /// `OutOfRange`. We recommend using `OutOfRange` (the more specific error)
120 /// when it applies so that callers who are iterating through a space can
121 /// easily look for an `OutOfRange` error to detect when they are done.
122 OutOfRange,
123
124 /// The operation is not implemented or is not supported/enabled in this
125 /// service.
126 Unimplemented,
127
128 /// Internal errors.
129 ///
130 /// This means that some invariants expected by the underlying system have
131 /// been broken. This error code is reserved for serious errors.
132 Internal,
133
134 /// The service is currently unavailable.
135 ///
136 /// This is most likely a transient condition, which can be corrected by
137 /// retrying with a backoff. Note that it is not always safe to retry
138 /// non-idempotent operations.
139 Unavailable,
140
141 /// Unrecoverable data loss or corruption.
142 DataLoss,
143
144 /// The request does not have valid authentication credentials for the
145 /// operation.
146 Unauthenticated,
147
148 /// Other codes
149 Other(u16),
150}
151
152impl From<u16> for Code {
153 #[inline]
154 fn from(value: u16) -> Self {
155 match value {
156 0 => Code::Ok,
157 1 => Code::Cancelled,
158 2 => Code::Unknown,
159 3 => Code::InvalidArgument,
160 4 => Code::DeadlineExceeded,
161 5 => Code::NotFound,
162 6 => Code::AlreadyExists,
163 7 => Code::PermissionDenied,
164 8 => Code::ResourceExhausted,
165 9 => Code::FailedPrecondition,
166 10 => Code::Aborted,
167 11 => Code::OutOfRange,
168 12 => Code::Unimplemented,
169 13 => Code::Internal,
170 14 => Code::Unavailable,
171 15 => Code::DataLoss,
172 16 => Code::Unauthenticated,
173 _ => Code::Other(value),
174 }
175 }
176}
177
178impl Code {
179 /// Returns the code number as a `u16`
180 #[inline]
181 pub fn as_u16(&self) -> u16 {
182 match self {
183 Code::Ok => 0,
184 Code::Cancelled => 1,
185 Code::Unknown => 2,
186 Code::InvalidArgument => 3,
187 Code::DeadlineExceeded => 4,
188 Code::NotFound => 5,
189 Code::AlreadyExists => 6,
190 Code::PermissionDenied => 7,
191 Code::ResourceExhausted => 8,
192 Code::FailedPrecondition => 9,
193 Code::Aborted => 10,
194 Code::OutOfRange => 11,
195 Code::Unimplemented => 12,
196 Code::Internal => 13,
197 Code::Unavailable => 14,
198 Code::DataLoss => 15,
199 Code::Unauthenticated => 16,
200 Code::Other(value) => *value,
201 }
202 }
203
204 fn header_value(&self) -> HeaderValue {
205 match self {
206 Code::Ok => HeaderValue::from_static("0"),
207 Code::Cancelled => HeaderValue::from_static("1"),
208 Code::Unknown => HeaderValue::from_static("2"),
209 Code::InvalidArgument => HeaderValue::from_static("3"),
210 Code::DeadlineExceeded => HeaderValue::from_static("4"),
211 Code::NotFound => HeaderValue::from_static("5"),
212 Code::AlreadyExists => HeaderValue::from_static("6"),
213 Code::PermissionDenied => HeaderValue::from_static("7"),
214 Code::ResourceExhausted => HeaderValue::from_static("8"),
215 Code::FailedPrecondition => HeaderValue::from_static("9"),
216 Code::Aborted => HeaderValue::from_static("10"),
217 Code::OutOfRange => HeaderValue::from_static("11"),
218 Code::Unimplemented => HeaderValue::from_static("12"),
219 Code::Internal => HeaderValue::from_static("13"),
220 Code::Unavailable => HeaderValue::from_static("14"),
221 Code::DataLoss => HeaderValue::from_static("15"),
222 Code::Unauthenticated => HeaderValue::from_static("16"),
223 Code::Other(code) => {
224 let mut buffer = itoa::Buffer::new();
225 buffer.format(*code).parse().unwrap()
226 }
227 }
228 }
229}
230
231/// A GRPC status describing the result of an RPC call.
232#[derive(Debug, Clone)]
233pub struct Status {
234 code: Code,
235 message: Option<String>,
236 metadata: Metadata,
237}
238
239impl Display for Status {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 if let Some(message) = &self.message {
242 write!(
243 f,
244 "grpc status: code={}, message={}",
245 self.code.as_u16(),
246 message
247 )
248 } else {
249 write!(f, "grpc status: code={}", self.code.as_u16())
250 }
251 }
252}
253
254impl std::error::Error for Status {}
255
256impl Status {
257 /// Create a `Status` with code
258 #[inline]
259 pub fn new(code: Code) -> Self {
260 Self {
261 code,
262 message: None,
263 metadata: Default::default(),
264 }
265 }
266
267 /// Create a `Status` from an error that implements [`std::error::Error`].
268 #[inline]
269 pub fn from_std_error<T: std::error::Error>(err: T) -> Self {
270 Status::new(Code::Internal).with_message(err)
271 }
272
273 /// Returns the code of this status
274 #[inline]
275 pub fn code(&self) -> Code {
276 self.code
277 }
278
279 /// Returns `true` if the code is `Ok(0)`
280 #[inline]
281 pub fn is_ok(&self) -> bool {
282 self.code == Code::Ok
283 }
284
285 /// Returns the reference to the error message.
286 #[inline]
287 pub fn message(&self) -> Option<&str> {
288 self.message.as_deref()
289 }
290
291 /// Attach a error message to this status.
292 #[inline]
293 pub fn with_message(self, message: impl Display) -> Self {
294 Self {
295 message: Some(message.to_string()),
296 ..self
297 }
298 }
299
300 /// Attach a meta data to this status.
301 #[inline]
302 pub fn with_metadata(self, metadata: Metadata) -> Self {
303 Self { metadata, ..self }
304 }
305
306 /// Returns a reference to the metadata.
307 #[inline]
308 pub fn metadata(&self) -> &Metadata {
309 &self.metadata
310 }
311
312 /// Returns a mutable reference to the metadata.
313 #[inline]
314 pub fn metadata_mut(&mut self) -> &mut Metadata {
315 &mut self.metadata
316 }
317
318 pub(crate) fn to_headers(&self) -> HeaderMap {
319 let mut headers = HeaderMap::new();
320
321 headers.insert(GRPC_STATUS_HEADER_CODE, self.code.header_value());
322
323 if let Some(message) = self
324 .message
325 .as_ref()
326 .map(|message| percent_encode(message.as_bytes(), ENCODING_SET).to_string())
327 .and_then(|encoded_message| HeaderValue::from_maybe_shared(encoded_message).ok())
328 {
329 headers.insert(GRPC_STATUS_MESSAGE_HEADER, message);
330 }
331
332 headers
333 }
334
335 #[allow(clippy::result_large_err)]
336 pub(crate) fn from_headers(headers: &HeaderMap) -> Result<Option<Status>, Status> {
337 if let Some(value) = headers.get(GRPC_STATUS_HEADER_CODE) {
338 let code: Code = value
339 .to_str()
340 .ok()
341 .and_then(|value| value.parse::<u16>().ok())
342 .ok_or_else(|| Status::new(Code::Internal).with_message("invalid grpc-status"))?
343 .into();
344 let mut status = Status::new(code);
345 if let Some(message) = headers
346 .get(GRPC_STATUS_MESSAGE_HEADER)
347 .and_then(|value| value.to_str().ok())
348 .and_then(|value| percent_decode_str(value).decode_utf8().ok())
349 {
350 status = status.with_message(message);
351 }
352 Ok(Some(status))
353 } else {
354 Ok(None)
355 }
356 }
357}