tower_http/classify/
grpc_errors_as_failures.rs

1use super::{ClassifiedResponse, ClassifyEos, ClassifyResponse, SharedClassifier};
2use bitflags::bitflags;
3use http::{HeaderMap, Response};
4use std::{fmt, num::NonZeroI32};
5
6/// gRPC status codes.
7///
8/// These variants match the [gRPC status codes].
9///
10/// [gRPC status codes]: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc
11#[derive(Clone, Copy, Debug)]
12pub enum GrpcCode {
13    /// The operation completed successfully.
14    Ok,
15    /// The operation was cancelled.
16    Cancelled,
17    /// Unknown error.
18    Unknown,
19    /// Client specified an invalid argument.
20    InvalidArgument,
21    /// Deadline expired before operation could complete.
22    DeadlineExceeded,
23    /// Some requested entity was not found.
24    NotFound,
25    /// Some entity that we attempted to create already exists.
26    AlreadyExists,
27    /// The caller does not have permission to execute the specified operation.
28    PermissionDenied,
29    /// Some resource has been exhausted.
30    ResourceExhausted,
31    /// The system is not in a state required for the operation's execution.
32    FailedPrecondition,
33    /// The operation was aborted.
34    Aborted,
35    /// Operation was attempted past the valid range.
36    OutOfRange,
37    /// Operation is not implemented or not supported.
38    Unimplemented,
39    /// Internal error.
40    Internal,
41    /// The service is currently unavailable.
42    Unavailable,
43    /// Unrecoverable data loss or corruption.
44    DataLoss,
45    /// The request does not have valid authentication credentials
46    Unauthenticated,
47}
48
49impl GrpcCode {
50    pub(crate) fn into_bitmask(self) -> GrpcCodeBitmask {
51        match self {
52            Self::Ok => GrpcCodeBitmask::OK,
53            Self::Cancelled => GrpcCodeBitmask::CANCELLED,
54            Self::Unknown => GrpcCodeBitmask::UNKNOWN,
55            Self::InvalidArgument => GrpcCodeBitmask::INVALID_ARGUMENT,
56            Self::DeadlineExceeded => GrpcCodeBitmask::DEADLINE_EXCEEDED,
57            Self::NotFound => GrpcCodeBitmask::NOT_FOUND,
58            Self::AlreadyExists => GrpcCodeBitmask::ALREADY_EXISTS,
59            Self::PermissionDenied => GrpcCodeBitmask::PERMISSION_DENIED,
60            Self::ResourceExhausted => GrpcCodeBitmask::RESOURCE_EXHAUSTED,
61            Self::FailedPrecondition => GrpcCodeBitmask::FAILED_PRECONDITION,
62            Self::Aborted => GrpcCodeBitmask::ABORTED,
63            Self::OutOfRange => GrpcCodeBitmask::OUT_OF_RANGE,
64            Self::Unimplemented => GrpcCodeBitmask::UNIMPLEMENTED,
65            Self::Internal => GrpcCodeBitmask::INTERNAL,
66            Self::Unavailable => GrpcCodeBitmask::UNAVAILABLE,
67            Self::DataLoss => GrpcCodeBitmask::DATA_LOSS,
68            Self::Unauthenticated => GrpcCodeBitmask::UNAUTHENTICATED,
69        }
70    }
71}
72
73bitflags! {
74    #[derive(Debug, Clone, Copy)]
75    pub(crate) struct GrpcCodeBitmask: u32 {
76        const OK                  = 0b00000000000000001;
77        const CANCELLED           = 0b00000000000000010;
78        const UNKNOWN             = 0b00000000000000100;
79        const INVALID_ARGUMENT    = 0b00000000000001000;
80        const DEADLINE_EXCEEDED   = 0b00000000000010000;
81        const NOT_FOUND           = 0b00000000000100000;
82        const ALREADY_EXISTS      = 0b00000000001000000;
83        const PERMISSION_DENIED   = 0b00000000010000000;
84        const RESOURCE_EXHAUSTED  = 0b00000000100000000;
85        const FAILED_PRECONDITION = 0b00000001000000000;
86        const ABORTED             = 0b00000010000000000;
87        const OUT_OF_RANGE        = 0b00000100000000000;
88        const UNIMPLEMENTED       = 0b00001000000000000;
89        const INTERNAL            = 0b00010000000000000;
90        const UNAVAILABLE         = 0b00100000000000000;
91        const DATA_LOSS           = 0b01000000000000000;
92        const UNAUTHENTICATED     = 0b10000000000000000;
93    }
94}
95
96impl GrpcCodeBitmask {
97    fn try_from_u32(code: u32) -> Option<Self> {
98        match code {
99            0 => Some(Self::OK),
100            1 => Some(Self::CANCELLED),
101            2 => Some(Self::UNKNOWN),
102            3 => Some(Self::INVALID_ARGUMENT),
103            4 => Some(Self::DEADLINE_EXCEEDED),
104            5 => Some(Self::NOT_FOUND),
105            6 => Some(Self::ALREADY_EXISTS),
106            7 => Some(Self::PERMISSION_DENIED),
107            8 => Some(Self::RESOURCE_EXHAUSTED),
108            9 => Some(Self::FAILED_PRECONDITION),
109            10 => Some(Self::ABORTED),
110            11 => Some(Self::OUT_OF_RANGE),
111            12 => Some(Self::UNIMPLEMENTED),
112            13 => Some(Self::INTERNAL),
113            14 => Some(Self::UNAVAILABLE),
114            15 => Some(Self::DATA_LOSS),
115            16 => Some(Self::UNAUTHENTICATED),
116            _ => None,
117        }
118    }
119}
120
121/// Response classifier for gRPC responses.
122///
123/// gRPC doesn't use normal HTTP statuses for indicating success or failure but instead a special
124/// header that might appear in a trailer.
125///
126/// Responses are considered successful if
127///
128/// - `grpc-status` header value contains a success value.
129/// default).
130/// - `grpc-status` header is missing.
131/// - `grpc-status` header value isn't a valid `String`.
132/// - `grpc-status` header value can't parsed into an `i32`.
133///
134/// All others are considered failures.
135#[derive(Debug, Clone)]
136pub struct GrpcErrorsAsFailures {
137    success_codes: GrpcCodeBitmask,
138}
139
140impl Default for GrpcErrorsAsFailures {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl GrpcErrorsAsFailures {
147    /// Create a new [`GrpcErrorsAsFailures`].
148    pub fn new() -> Self {
149        Self {
150            success_codes: GrpcCodeBitmask::OK,
151        }
152    }
153
154    /// Change which gRPC codes are considered success.
155    ///
156    /// Defaults to only considering `Ok` as success.
157    ///
158    /// `Ok` will always be considered a success.
159    ///
160    /// # Example
161    ///
162    /// Servers might not want to consider `Invalid Argument` or `Not Found` as failures since
163    /// thats likely the clients fault:
164    ///
165    /// ```rust
166    /// use tower_http::classify::{GrpcErrorsAsFailures, GrpcCode};
167    ///
168    /// let classifier = GrpcErrorsAsFailures::new()
169    ///     .with_success(GrpcCode::InvalidArgument)
170    ///     .with_success(GrpcCode::NotFound);
171    /// ```
172    pub fn with_success(mut self, code: GrpcCode) -> Self {
173        self.success_codes |= code.into_bitmask();
174        self
175    }
176
177    /// Returns a [`MakeClassifier`](super::MakeClassifier) that produces `GrpcErrorsAsFailures`.
178    ///
179    /// This is a convenience function that simply calls `SharedClassifier::new`.
180    pub fn make_classifier() -> SharedClassifier<Self> {
181        SharedClassifier::new(Self::new())
182    }
183}
184
185impl ClassifyResponse for GrpcErrorsAsFailures {
186    type FailureClass = GrpcFailureClass;
187    type ClassifyEos = GrpcEosErrorsAsFailures;
188
189    fn classify_response<B>(
190        self,
191        res: &Response<B>,
192    ) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos> {
193        match classify_grpc_metadata(res.headers(), self.success_codes) {
194            ParsedGrpcStatus::Success
195            | ParsedGrpcStatus::HeaderNotString
196            | ParsedGrpcStatus::HeaderNotInt => ClassifiedResponse::Ready(Ok(())),
197            ParsedGrpcStatus::NonSuccess(status) => {
198                ClassifiedResponse::Ready(Err(GrpcFailureClass::Code(status)))
199            }
200            ParsedGrpcStatus::GrpcStatusHeaderMissing => {
201                ClassifiedResponse::RequiresEos(GrpcEosErrorsAsFailures {
202                    success_codes: self.success_codes,
203                })
204            }
205        }
206    }
207
208    fn classify_error<E>(self, error: &E) -> Self::FailureClass
209    where
210        E: fmt::Display + 'static,
211    {
212        GrpcFailureClass::Error(error.to_string())
213    }
214}
215
216/// The [`ClassifyEos`] for [`GrpcErrorsAsFailures`].
217#[derive(Debug, Clone)]
218pub struct GrpcEosErrorsAsFailures {
219    success_codes: GrpcCodeBitmask,
220}
221
222impl ClassifyEos for GrpcEosErrorsAsFailures {
223    type FailureClass = GrpcFailureClass;
224
225    fn classify_eos(self, trailers: Option<&HeaderMap>) -> Result<(), Self::FailureClass> {
226        if let Some(trailers) = trailers {
227            match classify_grpc_metadata(trailers, self.success_codes) {
228                ParsedGrpcStatus::Success
229                | ParsedGrpcStatus::GrpcStatusHeaderMissing
230                | ParsedGrpcStatus::HeaderNotString
231                | ParsedGrpcStatus::HeaderNotInt => Ok(()),
232                ParsedGrpcStatus::NonSuccess(status) => Err(GrpcFailureClass::Code(status)),
233            }
234        } else {
235            Ok(())
236        }
237    }
238
239    fn classify_error<E>(self, error: &E) -> Self::FailureClass
240    where
241        E: fmt::Display + 'static,
242    {
243        GrpcFailureClass::Error(error.to_string())
244    }
245}
246
247/// The failure class for [`GrpcErrorsAsFailures`].
248#[derive(Debug)]
249pub enum GrpcFailureClass {
250    /// A gRPC response was classified as a failure with the corresponding status.
251    Code(std::num::NonZeroI32),
252    /// A gRPC response was classified as an error with the corresponding error description.
253    Error(String),
254}
255
256impl fmt::Display for GrpcFailureClass {
257    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
258        match self {
259            Self::Code(code) => write!(f, "Code: {}", code),
260            Self::Error(error) => write!(f, "Error: {}", error),
261        }
262    }
263}
264
265pub(crate) fn classify_grpc_metadata(
266    headers: &HeaderMap,
267    success_codes: GrpcCodeBitmask,
268) -> ParsedGrpcStatus {
269    macro_rules! or_else {
270        ($expr:expr, $other:ident) => {
271            if let Some(value) = $expr {
272                value
273            } else {
274                return ParsedGrpcStatus::$other;
275            }
276        };
277    }
278
279    let status = or_else!(headers.get("grpc-status"), GrpcStatusHeaderMissing);
280    let status = or_else!(status.to_str().ok(), HeaderNotString);
281    let status = or_else!(status.parse::<i32>().ok(), HeaderNotInt);
282
283    if GrpcCodeBitmask::try_from_u32(status as _)
284        .filter(|code| success_codes.contains(*code))
285        .is_some()
286    {
287        ParsedGrpcStatus::Success
288    } else {
289        ParsedGrpcStatus::NonSuccess(NonZeroI32::new(status).unwrap())
290    }
291}
292
293#[derive(Debug, PartialEq, Eq)]
294pub(crate) enum ParsedGrpcStatus {
295    Success,
296    NonSuccess(NonZeroI32),
297    GrpcStatusHeaderMissing,
298    // these two are treated as `Success` but kept separate for clarity
299    HeaderNotString,
300    HeaderNotInt,
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    macro_rules! classify_grpc_metadata_test {
308        (
309            name: $name:ident,
310            status: $status:expr,
311            success_flags: $success_flags:expr,
312            expected: $expected:expr,
313        ) => {
314            #[test]
315            fn $name() {
316                let mut headers = HeaderMap::new();
317                headers.insert("grpc-status", $status.parse().unwrap());
318                let status = classify_grpc_metadata(&headers, $success_flags);
319                assert_eq!(status, $expected);
320            }
321        };
322    }
323
324    classify_grpc_metadata_test! {
325        name: basic_ok,
326        status: "0",
327        success_flags: GrpcCodeBitmask::OK,
328        expected: ParsedGrpcStatus::Success,
329    }
330
331    classify_grpc_metadata_test! {
332        name: basic_error,
333        status: "1",
334        success_flags: GrpcCodeBitmask::OK,
335        expected: ParsedGrpcStatus::NonSuccess(NonZeroI32::new(1).unwrap()),
336    }
337
338    classify_grpc_metadata_test! {
339        name: two_success_codes_first_matches,
340        status: "0",
341        success_flags: GrpcCodeBitmask::OK | GrpcCodeBitmask::INVALID_ARGUMENT,
342        expected: ParsedGrpcStatus::Success,
343    }
344
345    classify_grpc_metadata_test! {
346        name: two_success_codes_second_matches,
347        status: "3",
348        success_flags: GrpcCodeBitmask::OK | GrpcCodeBitmask::INVALID_ARGUMENT,
349        expected: ParsedGrpcStatus::Success,
350    }
351
352    classify_grpc_metadata_test! {
353        name: two_success_codes_none_matches,
354        status: "16",
355        success_flags: GrpcCodeBitmask::OK | GrpcCodeBitmask::INVALID_ARGUMENT,
356        expected: ParsedGrpcStatus::NonSuccess(NonZeroI32::new(16).unwrap()),
357    }
358}