Coverage Report

Created: 2026-06-28 08:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/tower-http-0.6.10/src/classify/mod.rs
Line
Count
Source
1
//! Tools for classifying responses as either success or failure.
2
3
use http::{HeaderMap, Request, Response, StatusCode};
4
use std::{convert::Infallible, fmt, marker::PhantomData};
5
6
pub(crate) mod grpc_errors_as_failures;
7
mod map_failure_class;
8
mod status_in_range_is_error;
9
10
pub 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
27
pub 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)]
101
pub struct SharedClassifier<C> {
102
    classifier: C,
103
}
104
105
impl<C> SharedClassifier<C> {
106
    /// Create a new `SharedClassifier` from the given classifier.
107
0
    pub fn new(classifier: C) -> Self
108
0
    where
109
0
        C: ClassifyResponse + Clone,
110
    {
111
0
        Self { classifier }
112
0
    }
Unexecuted instantiation: <tower_http::classify::SharedClassifier<tower_http::classify::ServerErrorsAsFailures>>::new
Unexecuted instantiation: <tower_http::classify::SharedClassifier<tower_http::classify::grpc_errors_as_failures::GrpcErrorsAsFailures>>::new
Unexecuted instantiation: <tower_http::classify::SharedClassifier<tower_http::classify::status_in_range_is_error::StatusInRangeAsFailures>>::new
113
}
114
115
impl<C> MakeClassifier for SharedClassifier<C>
116
where
117
    C: ClassifyResponse + Clone,
118
{
119
    type FailureClass = C::FailureClass;
120
    type ClassifyEos = C::ClassifyEos;
121
    type Classifier = C;
122
123
0
    fn make_classifier<B>(&self, _req: &Request<B>) -> Self::Classifier {
124
0
        self.classifier.clone()
125
0
    }
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
142
pub 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
0
    fn map_failure_class<F, NewClass>(self, f: F) -> MapFailureClass<Self, F>
232
0
    where
233
0
        Self: Sized,
234
0
        F: FnOnce(Self::FailureClass) -> NewClass,
235
    {
236
0
        MapFailureClass::new(self, f)
237
0
    }
238
}
239
240
/// Trait for classifying end of streams (EOS) as either success or failure.
241
pub 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
0
    fn map_failure_class<F, NewClass>(self, f: F) -> MapFailureClass<Self, F>
260
0
    where
261
0
        Self: Sized,
262
0
        F: FnOnce(Self::FailureClass) -> NewClass,
263
    {
264
0
        MapFailureClass::new(self, f)
265
0
    }
266
}
267
268
/// Result of doing a classification.
269
#[derive(Debug)]
270
pub 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.
281
pub struct NeverClassifyEos<T> {
282
    _output_ty: PhantomData<fn() -> T>,
283
    _never: Infallible,
284
}
285
286
impl<T> ClassifyEos for NeverClassifyEos<T> {
287
    type FailureClass = T;
288
289
0
    fn classify_eos(self, _trailers: Option<&HeaderMap>) -> Result<(), Self::FailureClass> {
290
        // `NeverClassifyEos` contains an `Infallible` so it can never be constructed
291
0
        unreachable!()
292
    }
293
294
0
    fn classify_error<E>(self, _error: &E) -> Self::FailureClass
295
0
    where
296
0
        E: fmt::Display + 'static,
297
    {
298
        // `NeverClassifyEos` contains an `Infallible` so it can never be constructed
299
0
        unreachable!()
300
    }
301
}
302
303
impl<T> fmt::Debug for NeverClassifyEos<T> {
304
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305
0
        f.debug_struct("NeverClassifyEos").finish()
306
0
    }
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)]
314
pub struct ServerErrorsAsFailures {
315
    _priv: (),
316
}
317
318
impl ServerErrorsAsFailures {
319
    /// Create a new [`ServerErrorsAsFailures`].
320
0
    pub fn new() -> Self {
321
0
        Self::default()
322
0
    }
323
324
    /// Returns a [`MakeClassifier`] that produces `ServerErrorsAsFailures`.
325
    ///
326
    /// This is a convenience function that simply calls `SharedClassifier::new`.
327
0
    pub fn make_classifier() -> SharedClassifier<Self> {
328
0
        SharedClassifier::new(Self::new())
329
0
    }
330
}
331
332
impl ClassifyResponse for ServerErrorsAsFailures {
333
    type FailureClass = ServerErrorsFailureClass;
334
    type ClassifyEos = NeverClassifyEos<ServerErrorsFailureClass>;
335
336
0
    fn classify_response<B>(
337
0
        self,
338
0
        res: &Response<B>,
339
0
    ) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos> {
340
0
        if res.status().is_server_error() {
341
0
            ClassifiedResponse::Ready(Err(ServerErrorsFailureClass::StatusCode(res.status())))
342
        } else {
343
0
            ClassifiedResponse::Ready(Ok(()))
344
        }
345
0
    }
346
347
0
    fn classify_error<E>(self, error: &E) -> Self::FailureClass
348
0
    where
349
0
        E: fmt::Display + 'static,
350
    {
351
0
        ServerErrorsFailureClass::Error(error.to_string())
352
0
    }
353
}
354
355
/// The failure class for [`ServerErrorsAsFailures`].
356
#[derive(Debug)]
357
pub 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
364
impl fmt::Display for ServerErrorsFailureClass {
365
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366
0
        match self {
367
0
            Self::StatusCode(code) => write!(f, "Status code: {}", code),
368
0
            Self::Error(error) => write!(f, "Error: {}", error),
369
        }
370
0
    }
371
}
372
373
// Just verify that we can actually use this response classifier to determine retries as well
374
#[cfg(test)]
375
mod usable_for_retries {
376
    #![allow(dead_code)]
377
378
    use std::fmt;
379
380
    use http::{Request, Response};
381
    use tower::retry::Policy;
382
383
    use super::{ClassifiedResponse, ClassifyResponse};
384
385
    trait IsRetryable {
386
        fn is_retryable(&self) -> bool;
387
    }
388
389
    #[derive(Clone)]
390
    struct RetryBasedOnClassification<C> {
391
        classifier: C,
392
        // ...
393
    }
394
395
    impl<ReqB, ResB, E, C> Policy<Request<ReqB>, Response<ResB>, E> for RetryBasedOnClassification<C>
396
    where
397
        C: ClassifyResponse + Clone,
398
        E: fmt::Display + 'static,
399
        C::FailureClass: IsRetryable,
400
        ResB: http_body::Body,
401
        Request<ReqB>: Clone,
402
        E: std::error::Error + 'static,
403
    {
404
        type Future = std::future::Ready<()>;
405
406
        fn retry(
407
            &mut self,
408
            _req: &mut Request<ReqB>,
409
            res: &mut Result<Response<ResB>, E>,
410
        ) -> Option<Self::Future> {
411
            match res {
412
                Ok(res) => {
413
                    if let ClassifiedResponse::Ready(class) =
414
                        self.classifier.clone().classify_response(res)
415
                    {
416
                        if class.err()?.is_retryable() {
417
                            return Some(std::future::ready(()));
418
                        }
419
                    }
420
421
                    None
422
                }
423
                Err(err) => self
424
                    .classifier
425
                    .clone()
426
                    .classify_error(err)
427
                    .is_retryable()
428
                    .then(|| std::future::ready(())),
429
            }
430
        }
431
432
        fn clone_request(&mut self, req: &Request<ReqB>) -> Option<Request<ReqB>> {
433
            Some(req.clone())
434
        }
435
    }
436
}