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}