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}