Skip to main content

rama_http/layer/classify/
mod.rs

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