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}