rama_http/layer/classify/
mod.rs

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