/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 | | } |