tower_http/classify/
mod.rs

1//! Tools for classifying responses as either success or failure.
2
3use http::{HeaderMap, Request, Response, StatusCode};
4use std::{convert::Infallible, fmt, marker::PhantomData};
5
6pub(crate) mod grpc_errors_as_failures;
7mod map_failure_class;
8mod status_in_range_is_error;
9
10pub use self::{
11    grpc_errors_as_failures::{
12        GrpcCode, GrpcEosErrorsAsFailures, GrpcErrorsAsFailures, GrpcFailureClass,
13    },
14    map_failure_class::MapFailureClass,
15    status_in_range_is_error::{StatusInRangeAsFailures, StatusInRangeFailureClass},
16};
17
18/// Trait for producing response classifiers from a request.
19///
20/// This is useful when a classifier depends on data from the request. For example, this could
21/// include the URI or HTTP method.
22///
23/// This trait is generic over the [`Error` type] of the `Service`s used with the classifier.
24/// This is necessary for [`ClassifyResponse::classify_error`].
25///
26/// [`Error` type]: https://docs.rs/tower/latest/tower/trait.Service.html#associatedtype.Error
27pub trait MakeClassifier {
28    /// The response classifier produced.
29    type Classifier: ClassifyResponse<
30        FailureClass = Self::FailureClass,
31        ClassifyEos = Self::ClassifyEos,
32    >;
33
34    /// The type of failure classifications.
35    ///
36    /// This might include additional information about the error, such as
37    /// whether it was a client or server error, or whether or not it should
38    /// be considered retryable.
39    type FailureClass;
40
41    /// The type used to classify the response end of stream (EOS).
42    type ClassifyEos: ClassifyEos<FailureClass = Self::FailureClass>;
43
44    /// Returns a response classifier for this request
45    fn make_classifier<B>(&self, req: &Request<B>) -> Self::Classifier;
46}
47
48/// A [`MakeClassifier`] that produces new classifiers by cloning an inner classifier.
49///
50/// When a type implementing [`ClassifyResponse`] doesn't depend on information
51/// from the request, [`SharedClassifier`] can be used to turn an instance of that type
52/// into a [`MakeClassifier`].
53///
54/// # Example
55///
56/// ```
57/// use std::fmt;
58/// use tower_http::classify::{
59///     ClassifyResponse, ClassifiedResponse, NeverClassifyEos,
60///     SharedClassifier, MakeClassifier,
61/// };
62/// use http::Response;
63///
64/// // A response classifier that only considers errors to be failures.
65/// #[derive(Clone, Copy)]
66/// struct MyClassifier;
67///
68/// impl ClassifyResponse for MyClassifier {
69///     type FailureClass = String;
70///     type ClassifyEos = NeverClassifyEos<Self::FailureClass>;
71///
72///     fn classify_response<B>(
73///         self,
74///         _res: &Response<B>,
75///     ) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos> {
76///         ClassifiedResponse::Ready(Ok(()))
77///     }
78///
79///     fn classify_error<E>(self, error: &E) -> Self::FailureClass
80///     where
81///         E: fmt::Display + 'static,
82///     {
83///         error.to_string()
84///     }
85/// }
86///
87/// // Some function that requires a `MakeClassifier`
88/// fn use_make_classifier<M: MakeClassifier>(make: M) {
89///     // ...
90/// }
91///
92/// // `MyClassifier` doesn't implement `MakeClassifier` but since it doesn't
93/// // care about the incoming request we can make `MyClassifier`s by cloning.
94/// // That is what `SharedClassifier` does.
95/// let make_classifier = SharedClassifier::new(MyClassifier);
96///
97/// // We now have a `MakeClassifier`!
98/// use_make_classifier(make_classifier);
99/// ```
100#[derive(Debug, Clone)]
101pub struct SharedClassifier<C> {
102    classifier: C,
103}
104
105impl<C> SharedClassifier<C> {
106    /// Create a new `SharedClassifier` from the given classifier.
107    pub fn new(classifier: C) -> Self
108    where
109        C: ClassifyResponse + Clone,
110    {
111        Self { classifier }
112    }
113}
114
115impl<C> MakeClassifier for SharedClassifier<C>
116where
117    C: ClassifyResponse + Clone,
118{
119    type FailureClass = C::FailureClass;
120    type ClassifyEos = C::ClassifyEos;
121    type Classifier = C;
122
123    fn make_classifier<B>(&self, _req: &Request<B>) -> Self::Classifier {
124        self.classifier.clone()
125    }
126}
127
128/// Trait for classifying responses as either success or failure. Designed to support both unary
129/// requests (single request for a single response) as well as streaming responses.
130///
131/// Response classifiers are used in cases where middleware needs to determine
132/// whether a response completed successfully or failed. For example, they may
133/// be used by logging or metrics middleware to record failures differently
134/// from successes.
135///
136/// Furthermore, when a response fails, a response classifier may provide
137/// additional information about the failure. This can, for example, be used to
138/// build [retry policies] by indicating whether or not a particular failure is
139/// retryable.
140///
141/// [retry policies]: https://docs.rs/tower/latest/tower/retry/trait.Policy.html
142pub trait ClassifyResponse {
143    /// The type returned when a response is classified as a failure.
144    ///
145    /// Depending on the classifier, this may simply indicate that the
146    /// request failed, or it may contain additional  information about
147    /// the failure, such as whether or not it is retryable.
148    type FailureClass;
149
150    /// The type used to classify the response end of stream (EOS).
151    type ClassifyEos: ClassifyEos<FailureClass = Self::FailureClass>;
152
153    /// Attempt to classify the beginning of a response.
154    ///
155    /// In some cases, the response can be classified immediately, without
156    /// waiting for a body to complete. This may include:
157    ///
158    /// - When the response has an error status code.
159    /// - When a successful response does not have a streaming body.
160    /// - When the classifier does not care about streaming bodies.
161    ///
162    /// When the response can be classified immediately, `classify_response`
163    /// returns a [`ClassifiedResponse::Ready`] which indicates whether the
164    /// response succeeded or failed.
165    ///
166    /// In other cases, however, the classifier may need to wait until the
167    /// response body stream completes before it can classify the response.
168    /// For example, gRPC indicates RPC failures using the `grpc-status`
169    /// trailer. In this case, `classify_response` returns a
170    /// [`ClassifiedResponse::RequiresEos`] containing a type which will
171    /// be used to classify the response when the body stream ends.
172    fn classify_response<B>(
173        self,
174        res: &Response<B>,
175    ) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos>;
176
177    /// Classify an error.
178    ///
179    /// Errors are always errors (doh) but sometimes it might be useful to have multiple classes of
180    /// errors. A retry policy might allow retrying some errors and not others.
181    fn classify_error<E>(self, error: &E) -> Self::FailureClass
182    where
183        E: fmt::Display + 'static;
184
185    /// Transform the failure classification using a function.
186    ///
187    /// # Example
188    ///
189    /// ```
190    /// use tower_http::classify::{
191    ///     ServerErrorsAsFailures, ServerErrorsFailureClass,
192    ///     ClassifyResponse, ClassifiedResponse
193    /// };
194    /// use http::{Response, StatusCode};
195    /// use http_body_util::Empty;
196    /// use bytes::Bytes;
197    ///
198    /// fn transform_failure_class(class: ServerErrorsFailureClass) -> NewFailureClass {
199    ///     match class {
200    ///         // Convert status codes into u16
201    ///         ServerErrorsFailureClass::StatusCode(status) => {
202    ///             NewFailureClass::Status(status.as_u16())
203    ///         }
204    ///         // Don't change errors.
205    ///         ServerErrorsFailureClass::Error(error) => {
206    ///             NewFailureClass::Error(error)
207    ///         }
208    ///     }
209    /// }
210    ///
211    /// enum NewFailureClass {
212    ///     Status(u16),
213    ///     Error(String),
214    /// }
215    ///
216    /// // Create a classifier who's failure class will be transformed by `transform_failure_class`
217    /// let classifier = ServerErrorsAsFailures::new().map_failure_class(transform_failure_class);
218    ///
219    /// let response = Response::builder()
220    ///     .status(StatusCode::INTERNAL_SERVER_ERROR)
221    ///     .body(Empty::<Bytes>::new())
222    ///     .unwrap();
223    ///
224    /// let classification = classifier.classify_response(&response);
225    ///
226    /// assert!(matches!(
227    ///     classification,
228    ///     ClassifiedResponse::Ready(Err(NewFailureClass::Status(500)))
229    /// ));
230    /// ```
231    fn map_failure_class<F, NewClass>(self, f: F) -> MapFailureClass<Self, F>
232    where
233        Self: Sized,
234        F: FnOnce(Self::FailureClass) -> NewClass,
235    {
236        MapFailureClass::new(self, f)
237    }
238}
239
240/// Trait for classifying end of streams (EOS) as either success or failure.
241pub trait ClassifyEos {
242    /// The type of failure classifications.
243    type FailureClass;
244
245    /// Perform the classification from response trailers.
246    fn classify_eos(self, trailers: Option<&HeaderMap>) -> Result<(), Self::FailureClass>;
247
248    /// Classify an error.
249    ///
250    /// Errors are always errors (doh) but sometimes it might be useful to have multiple classes of
251    /// errors. A retry policy might allow retrying some errors and not others.
252    fn classify_error<E>(self, error: &E) -> Self::FailureClass
253    where
254        E: fmt::Display + 'static;
255
256    /// Transform the failure classification using a function.
257    ///
258    /// See [`ClassifyResponse::map_failure_class`] for more details.
259    fn map_failure_class<F, NewClass>(self, f: F) -> MapFailureClass<Self, F>
260    where
261        Self: Sized,
262        F: FnOnce(Self::FailureClass) -> NewClass,
263    {
264        MapFailureClass::new(self, f)
265    }
266}
267
268/// Result of doing a classification.
269#[derive(Debug)]
270pub enum ClassifiedResponse<FailureClass, ClassifyEos> {
271    /// The response was able to be classified immediately.
272    Ready(Result<(), FailureClass>),
273    /// We have to wait until the end of a streaming response to classify it.
274    RequiresEos(ClassifyEos),
275}
276
277/// A [`ClassifyEos`] type that can be used in [`ClassifyResponse`] implementations that never have
278/// to classify streaming responses.
279///
280/// `NeverClassifyEos` exists only as type.  `NeverClassifyEos` values cannot be constructed.
281pub struct NeverClassifyEos<T> {
282    _output_ty: PhantomData<fn() -> T>,
283    _never: Infallible,
284}
285
286impl<T> ClassifyEos for NeverClassifyEos<T> {
287    type FailureClass = T;
288
289    fn classify_eos(self, _trailers: Option<&HeaderMap>) -> Result<(), Self::FailureClass> {
290        // `NeverClassifyEos` contains an `Infallible` so it can never be constructed
291        unreachable!()
292    }
293
294    fn classify_error<E>(self, _error: &E) -> Self::FailureClass
295    where
296        E: fmt::Display + 'static,
297    {
298        // `NeverClassifyEos` contains an `Infallible` so it can never be constructed
299        unreachable!()
300    }
301}
302
303impl<T> fmt::Debug for NeverClassifyEos<T> {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        f.debug_struct("NeverClassifyEos").finish()
306    }
307}
308
309/// The default classifier used for normal HTTP responses.
310///
311/// Responses with a `5xx` status code are considered failures, all others are considered
312/// successes.
313#[derive(Clone, Debug, Default)]
314pub struct ServerErrorsAsFailures {
315    _priv: (),
316}
317
318impl ServerErrorsAsFailures {
319    /// Create a new [`ServerErrorsAsFailures`].
320    pub fn new() -> Self {
321        Self::default()
322    }
323
324    /// Returns a [`MakeClassifier`] that produces `ServerErrorsAsFailures`.
325    ///
326    /// This is a convenience function that simply calls `SharedClassifier::new`.
327    pub fn make_classifier() -> SharedClassifier<Self> {
328        SharedClassifier::new(Self::new())
329    }
330}
331
332impl ClassifyResponse for ServerErrorsAsFailures {
333    type FailureClass = ServerErrorsFailureClass;
334    type ClassifyEos = NeverClassifyEos<ServerErrorsFailureClass>;
335
336    fn classify_response<B>(
337        self,
338        res: &Response<B>,
339    ) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos> {
340        if res.status().is_server_error() {
341            ClassifiedResponse::Ready(Err(ServerErrorsFailureClass::StatusCode(res.status())))
342        } else {
343            ClassifiedResponse::Ready(Ok(()))
344        }
345    }
346
347    fn classify_error<E>(self, error: &E) -> Self::FailureClass
348    where
349        E: fmt::Display + 'static,
350    {
351        ServerErrorsFailureClass::Error(error.to_string())
352    }
353}
354
355/// The failure class for [`ServerErrorsAsFailures`].
356#[derive(Debug)]
357pub enum ServerErrorsFailureClass {
358    /// A response was classified as a failure with the corresponding status.
359    StatusCode(StatusCode),
360    /// A response was classified as an error with the corresponding error description.
361    Error(String),
362}
363
364impl fmt::Display for ServerErrorsFailureClass {
365    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
366        match self {
367            Self::StatusCode(code) => write!(f, "Status code: {}", code),
368            Self::Error(error) => write!(f, "Error: {}", error),
369        }
370    }
371}
372
373// Just verify that we can actually use this response classifier to determine retries as well
374#[cfg(test)]
375mod usable_for_retries {
376    #[allow(unused_imports)]
377    use super::*;
378    use http::{Request, Response};
379    use tower::retry::Policy;
380
381    trait IsRetryable {
382        fn is_retryable(&self) -> bool;
383    }
384
385    #[derive(Clone)]
386    struct RetryBasedOnClassification<C> {
387        classifier: C,
388        // ...
389    }
390
391    impl<ReqB, ResB, E, C> Policy<Request<ReqB>, Response<ResB>, E> for RetryBasedOnClassification<C>
392    where
393        C: ClassifyResponse + Clone,
394        E: fmt::Display + 'static,
395        C::FailureClass: IsRetryable,
396        ResB: http_body::Body,
397        Request<ReqB>: Clone,
398        E: std::error::Error + 'static,
399    {
400        type Future = std::future::Ready<()>;
401
402        fn retry(
403            &mut self,
404            _req: &mut Request<ReqB>,
405            res: &mut Result<Response<ResB>, E>,
406        ) -> Option<Self::Future> {
407            match res {
408                Ok(res) => {
409                    if let ClassifiedResponse::Ready(class) =
410                        self.classifier.clone().classify_response(res)
411                    {
412                        if class.err()?.is_retryable() {
413                            return Some(std::future::ready(()));
414                        }
415                    }
416
417                    None
418                }
419                Err(err) => self
420                    .classifier
421                    .clone()
422                    .classify_error(err)
423                    .is_retryable()
424                    .then(|| std::future::ready(())),
425            }
426        }
427
428        fn clone_request(&mut self, req: &Request<ReqB>) -> Option<Request<ReqB>> {
429            Some(req.clone())
430        }
431    }
432}