1use std::fmt;
19use std::sync::Arc;
20use std::task::{Context, Poll};
21use std::time::Duration;
22
23use bytes::{Bytes, BytesMut};
24use http::header::RETRY_AFTER;
25use http::{HeaderMap, StatusCode};
26use http_body_util::{BodyDataStream, BodyExt};
27use tower::retry::Policy;
28use tower::{Layer, Service};
29
30use crate::maybe::BoxFuture;
31use crate::timer::{since_epoch, Instant, Sleep};
32use crate::{Body, BoxError, MaybeSend, MaybeSync};
33
34const RETRY_AFTER_MS: &str = "retry-after-ms";
36
37pub const DEFAULT_BODY_LIMIT: usize = 64 * 1024;
39
40#[derive(Clone, Debug)]
55pub struct RetryPolicy {
56 max_retries: usize,
57 attempts: usize,
58 base_delay: Duration,
59 max_delay: Duration,
60 max_retry_after: Duration,
61 jitter: bool,
62 server_directed_429: bool,
63}
64
65impl RetryPolicy {
66 pub fn new(max_retries: usize) -> Self {
68 Self {
69 max_retries,
70 attempts: 0,
71 base_delay: Duration::from_millis(200),
72 max_delay: Duration::from_secs(8),
73 max_retry_after: Duration::from_secs(60),
74 jitter: true,
75 server_directed_429: false,
76 }
77 }
78
79 pub fn base_delay(mut self, delay: Duration) -> Self {
81 self.base_delay = delay;
82 self
83 }
84
85 pub fn max_delay(mut self, delay: Duration) -> Self {
88 self.max_delay = delay;
89 self
90 }
91
92 pub fn max_retry_after(mut self, delay: Duration) -> Self {
95 self.max_retry_after = delay;
96 self
97 }
98
99 pub fn jitter(mut self, jitter: bool) -> Self {
102 self.jitter = jitter;
103 self
104 }
105
106 pub fn server_directed_429(mut self, server_directed: bool) -> Self {
112 self.server_directed_429 = server_directed;
113 self
114 }
115
116 pub fn max_retries(&self) -> usize {
118 self.max_retries
119 }
120
121 fn deadline(&self, retry_after: Option<Instant>, now: Instant) -> Option<Instant> {
124 if let Some(deadline) = retry_after {
125 return (deadline.saturating_duration_since(now) <= self.max_retry_after)
126 .then_some(deadline);
127 }
128 Some(now + self.backoff())
129 }
130
131 fn backoff(&self) -> Duration {
133 let backoff = self
134 .base_delay
135 .saturating_mul(1u32 << self.attempts.min(16))
136 .min(self.max_delay);
137 if self.jitter {
138 jittered(backoff, random_unit())
139 } else {
140 backoff
141 }
142 }
143}
144
145impl Default for RetryPolicy {
147 fn default() -> Self {
148 Self::new(3)
149 }
150}
151
152fn jittered(backoff: Duration, unit: f64) -> Duration {
154 backoff.mul_f64(0.5 + 0.5 * unit)
155}
156
157fn random_unit() -> f64 {
159 use std::hash::{BuildHasher, Hasher};
160 let hash = std::collections::hash_map::RandomState::new()
161 .build_hasher()
162 .finish();
163 (hash >> 11) as f64 / (1u64 << 53) as f64
164}
165
166#[derive(Clone, Copy, Debug)]
169struct ReceivedAt(Instant);
170
171#[derive(Clone, Copy, Debug)]
174struct Clocks {
175 received_at: Instant,
176 now: Instant,
177 wall: Duration,
178}
179
180impl Clocks {
181 fn now<B>(response: &http::Response<B>) -> Self {
182 let wall = since_epoch();
184 let now = Instant::now();
185 let received_at = response
186 .extensions()
187 .get::<ReceivedAt>()
188 .map_or(now, |received| received.0);
189 Self {
190 received_at,
191 now,
192 wall,
193 }
194 }
195}
196
197fn retryable<B>(
199 result: &Result<http::Response<B>, BoxError>,
200 server_directed_429: bool,
201) -> Option<Option<Instant>> {
202 match result {
203 Ok(response) => {
204 let status = response.status();
205 let transient = status == StatusCode::TOO_MANY_REQUESTS
206 || status == StatusCode::REQUEST_TIMEOUT
207 || status.is_server_error();
208 let permanent = response.extensions().get::<Permanent>().is_some();
209 if !transient || permanent {
210 return None;
211 }
212 let deadline = retry_after(response.headers(), Clocks::now(response));
213 if server_directed_429 && status == StatusCode::TOO_MANY_REQUESTS && deadline.is_none()
214 {
215 #[cfg(feature = "tracing")]
216 tracing::debug!(
217 target: "timvw_rust_runtime::trace",
218 status = status.as_u16(),
219 "not retrying: a 429 without Retry-After (server_directed_429)"
220 );
221 return None;
222 }
223 Some(deadline)
224 }
225 Err(error) => is_connect(error.as_ref()).then_some(None),
226 }
227}
228
229fn retry_after(headers: &HeaderMap, clocks: Clocks) -> Option<Instant> {
232 let value = |name| Some(headers.get(name)?.to_str().ok()?.trim());
233 let delay = |value: &str, scale: f64| {
234 let delay = Duration::try_from_secs_f64(value.parse::<f64>().ok()? * scale).ok()?;
235 clocks.received_at.checked_add(delay)
236 };
237 if let Some(deadline) = value(RETRY_AFTER_MS).and_then(|value| delay(value, 0.001)) {
238 return Some(deadline);
239 }
240 let value = value(RETRY_AFTER.as_str())?;
241 delay(value, 1.0).or_else(|| {
242 let date = httpdate::parse_http_date(value).ok()?;
243 let date = date
244 .duration_since(std::time::UNIX_EPOCH)
245 .unwrap_or_default();
246 let delay = date.saturating_sub(clocks.wall);
247 clocks.now.checked_add(delay)
248 })
249}
250
251#[allow(unused_variables)]
254fn is_connect(error: &(dyn std::error::Error + 'static)) -> bool {
255 #[cfg(all(feature = "reqwest", not(target_family = "wasm")))]
256 {
257 let mut source = Some(error);
258 while let Some(error) = source {
259 if error
260 .downcast_ref::<reqwest::Error>()
261 .is_some_and(reqwest::Error::is_connect)
262 {
263 return true;
264 }
265 source = error.source();
266 }
267 }
268 false
269}
270
271impl<B> Policy<http::Request<Body>, http::Response<B>, BoxError> for RetryPolicy {
272 type Future = BoxFuture<'static, ()>;
273
274 fn retry(
275 &mut self,
276 _request: &mut http::Request<Body>,
277 result: &mut Result<http::Response<B>, BoxError>,
278 ) -> Option<Self::Future> {
279 if self.attempts >= self.max_retries {
280 return None;
281 }
282 let retry_after = retryable(result, self.server_directed_429)?;
283 let now = Instant::now();
284 let Some(deadline) = self.deadline(retry_after, now) else {
285 #[cfg(feature = "tracing")]
286 tracing::debug!(
287 target: "timvw_rust_runtime::trace",
288 retry_after_ms = retry_after
289 .map_or(0, |deadline| deadline.saturating_duration_since(now).as_millis() as u64),
290 max_retry_after_ms = self.max_retry_after.as_millis() as u64,
291 status = result.as_ref().ok().map(|response| response.status().as_u16()),
292 "not retrying: the server asks to wait longer than max_retry_after"
293 );
294 return None;
295 };
296 self.attempts += 1;
297 #[cfg(feature = "tracing")]
298 tracing::debug!(
299 target: "timvw_rust_runtime::trace",
300 attempt = self.attempts,
301 max_retries = self.max_retries,
302 delay_ms = deadline.saturating_duration_since(now).as_millis() as u64,
303 status = result.as_ref().ok().map(|response| response.status().as_u16()),
304 "retrying"
305 );
306 Some(Box::pin(Sleep::until(deadline)))
307 }
308
309 fn clone_request(&mut self, request: &http::Request<Body>) -> Option<http::Request<Body>> {
310 let body = request.body().try_clone()?;
311 let mut clone = http::Request::new(body);
312 *clone.method_mut() = request.method().clone();
313 *clone.uri_mut() = request.uri().clone();
314 *clone.version_mut() = request.version();
315 *clone.headers_mut() = request.headers().clone();
316 Some(clone)
317 }
318}
319
320#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
325pub struct Permanent;
326
327type Classifier = Arc<dyn Fn(&[u8]) -> bool + Send + Sync>;
329
330pub fn openai_insufficient_quota(body: &[u8]) -> bool {
335 const INSUFFICIENT_QUOTA: &str = "insufficient_quota";
336 let Ok(value) = serde_json::from_slice::<serde_json::Value>(body) else {
337 return false;
338 };
339 ["code", "type"].iter().any(|field| {
340 value
341 .pointer(&format!("/error/{field}"))
342 .and_then(serde_json::Value::as_str)
343 == Some(INSUFFICIENT_QUOTA)
344 })
345}
346
347#[derive(Clone)]
366pub struct RetryLayer {
367 policy: RetryPolicy,
368 classify: Option<Classifier>,
369 body_limit: usize,
370}
371
372impl RetryLayer {
373 pub fn new(policy: RetryPolicy) -> Self {
375 Self {
376 policy,
377 classify: None,
378 body_limit: DEFAULT_BODY_LIMIT,
379 }
380 }
381
382 pub fn permanent_429<F>(mut self, is_permanent: F) -> Self
387 where
388 F: Fn(&[u8]) -> bool + Send + Sync + 'static,
389 {
390 self.classify = Some(Arc::new(is_permanent));
391 self
392 }
393
394 pub fn body_limit(mut self, limit: usize) -> Self {
396 self.body_limit = limit;
397 self
398 }
399
400 pub fn policy(&self) -> &RetryPolicy {
402 &self.policy
403 }
404}
405
406impl Default for RetryLayer {
408 fn default() -> Self {
409 Self::new(RetryPolicy::default())
410 }
411}
412
413impl fmt::Debug for RetryLayer {
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415 f.debug_struct("RetryLayer")
416 .field("policy", &self.policy)
417 .field("permanent_429", &self.classify.is_some())
418 .field("body_limit", &self.body_limit)
419 .finish()
420 }
421}
422
423impl<S> Layer<S> for RetryLayer {
424 type Service = tower::retry::Retry<RetryPolicy, Classify429<S>>;
425
426 fn layer(&self, inner: S) -> Self::Service {
427 tower::retry::Retry::new(
428 self.policy.clone(),
429 Classify429 {
430 inner,
431 classify: self.classify.clone(),
432 body_limit: self.body_limit,
433 },
434 )
435 }
436}
437
438#[derive(Clone)]
440pub struct Classify429<S> {
441 inner: S,
442 classify: Option<Classifier>,
443 body_limit: usize,
444}
445
446impl<S: fmt::Debug> fmt::Debug for Classify429<S> {
447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448 f.debug_struct("Classify429")
449 .field("inner", &self.inner)
450 .field("permanent_429", &self.classify.is_some())
451 .field("body_limit", &self.body_limit)
452 .finish()
453 }
454}
455
456impl<S, R, B> Service<R> for Classify429<S>
457where
458 S: Service<R, Response = http::Response<B>>,
459 S::Future: MaybeSend + 'static,
460 B: http_body::Body + MaybeSend + MaybeSync + 'static,
461 B::Data: Into<Bytes>,
462 B::Error: Into<BoxError>,
463{
464 type Response = http::Response<Body>;
465 type Error = S::Error;
466 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
467
468 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
469 self.inner.poll_ready(cx)
470 }
471
472 fn call(&mut self, request: R) -> Self::Future {
473 let response = self.inner.call(request);
474 let classify = self.classify.clone();
475 let body_limit = self.body_limit;
476 Box::pin(async move {
477 let mut response = response.await?.map(Body::wrap);
478 Ok(match classify {
479 Some(classify) if response.status() == StatusCode::TOO_MANY_REQUESTS => {
480 response.extensions_mut().insert(ReceivedAt(Instant::now()));
481 classify_429(response, &*classify, body_limit).await
482 }
483 _ => response,
484 })
485 })
486 }
487}
488
489async fn classify_429(
492 response: http::Response<Body>,
493 classify: &(dyn Fn(&[u8]) -> bool + Send + Sync),
494 limit: usize,
495) -> http::Response<Body> {
496 let (mut parts, mut body) = response.into_parts();
497 let mut read = BytesMut::new();
498 let body = loop {
499 match body.frame().await {
500 None => {
501 let read = read.freeze();
502 if classify(&read) {
503 parts.extensions.insert(Permanent);
504 #[cfg(feature = "tracing")]
505 tracing::debug!(
506 target: "timvw_rust_runtime::trace",
507 status = parts.status.as_u16(),
508 "not retrying: the body says the error is permanent"
509 );
510 }
511 break Body::from(read);
512 }
513 Some(Ok(frame)) => {
515 let Ok(data) = frame.into_data() else {
516 continue;
517 };
518 read.extend_from_slice(&data);
519 if read.len() > limit {
520 let read = futures_util::stream::once(async move { Ok(read.freeze()) });
521 break Body::wrap_stream(futures_util::StreamExt::chain(
522 read,
523 BodyDataStream::new(body),
524 ));
525 }
526 }
527 Some(Err(error)) => {
528 break Body::wrap_stream(futures_util::stream::iter([
529 Ok(read.freeze()),
530 Err::<Bytes, BoxError>(error),
531 ]));
532 }
533 }
534 };
535 http::Response::from_parts(parts, body)
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 fn response(status: u16, headers: &[(&str, &str)]) -> Result<http::Response<()>, BoxError> {
543 let mut builder = http::Response::builder().status(status);
544 for (name, value) in headers {
545 builder = builder.header(*name, *value);
546 }
547 Ok(builder.body(()).unwrap())
548 }
549
550 fn wait(
552 result: &Result<http::Response<()>, BoxError>,
553 server_directed_429: bool,
554 ) -> Option<Option<Duration>> {
555 let now = Instant::now();
556 retryable(result, server_directed_429)
557 .map(|deadline| deadline.map(|deadline| deadline.saturating_duration_since(now)))
558 }
559
560 #[tokio::test(start_paused = true)]
561 async fn retries_transient_statuses_only() {
562 assert_eq!(wait(&response(429, &[]), false), Some(None));
563 assert_eq!(
564 wait(&response(503, &[("retry-after", "2")]), false),
565 Some(Some(Duration::from_secs(2)))
566 );
567 assert_eq!(wait(&response(408, &[]), false), Some(None));
568 assert_eq!(wait(&response(400, &[]), false), None);
569 assert_eq!(wait(&response(200, &[]), false), None);
570 assert_eq!(wait(&Err("boom".into()), false), None);
571 }
572
573 #[tokio::test(start_paused = true)]
574 async fn server_directed_429_needs_advice() {
575 assert_eq!(wait(&response(429, &[]), true), None);
576 assert_eq!(wait(&response(429, &[("retry-after", "soon")]), true), None);
577 assert_eq!(
578 wait(&response(429, &[("retry-after", "0")]), true),
579 Some(Some(Duration::ZERO))
580 );
581 assert_eq!(
582 wait(&response(429, &[("retry-after-ms", "300")]), true),
583 Some(Some(Duration::from_millis(300)))
584 );
585 let past = httpdate::fmt_http_date(std::time::SystemTime::now() - Duration::from_secs(60));
586 assert_eq!(
587 wait(&response(429, &[("retry-after", &past)]), true),
588 Some(Some(Duration::ZERO))
589 );
590 assert_eq!(wait(&response(503, &[]), true), Some(None));
592 assert_eq!(wait(&response(408, &[]), true), Some(None));
593 }
594
595 #[tokio::test(start_paused = true)]
596 async fn permanent_responses_are_not_retried() {
597 let mut result = response(429, &[("retry-after", "1")]);
598 result.as_mut().unwrap().extensions_mut().insert(Permanent);
599 assert_eq!(wait(&result, false), None);
600 }
601
602 fn clocks() -> Clocks {
603 let now = Instant::now();
604 Clocks {
605 received_at: now,
606 now,
607 wall: httpdate::parse_http_date("Wed, 21 Oct 2015 07:28:00 GMT")
608 .unwrap()
609 .duration_since(std::time::UNIX_EPOCH)
610 .unwrap(),
611 }
612 }
613
614 fn parse(headers: &[(&str, &str)], clocks: Clocks) -> Option<Duration> {
615 let mut map = HeaderMap::new();
616 for (name, value) in headers {
617 map.append(
618 http::HeaderName::from_bytes(name.as_bytes()).unwrap(),
619 value.parse().unwrap(),
620 );
621 }
622 retry_after(&map, clocks).map(|deadline| deadline.saturating_duration_since(clocks.now))
623 }
624
625 #[tokio::test(start_paused = true)]
626 async fn parses_retry_after_and_retry_after_ms() {
627 let wait = |headers: &[(&str, &str)]| parse(headers, clocks());
628 assert_eq!(
629 wait(&[("retry-after", " 20 ")]),
630 Some(Duration::from_secs(20))
631 );
632 assert_eq!(
633 wait(&[("retry-after", "1.5")]),
634 Some(Duration::from_millis(1500))
635 );
636 assert_eq!(
637 wait(&[("retry-after-ms", "250"), ("retry-after", "1")]),
638 Some(Duration::from_millis(250))
639 );
640 assert_eq!(
641 wait(&[("retry-after-ms", "12.5")]),
642 Some(Duration::from_micros(12_500))
643 );
644 assert_eq!(wait(&[("retry-after", "-1")]), None);
645 assert_eq!(wait(&[("retry-after", "")]), None);
646 assert_eq!(wait(&[("retry-after", "later")]), None);
647 assert_eq!(
648 wait(&[("retry-after-ms", "nan"), ("retry-after", "3")]),
649 Some(Duration::from_secs(3))
650 );
651 assert_eq!(wait(&[]), None);
652 }
653
654 #[tokio::test(start_paused = true)]
655 async fn parses_http_dates_against_the_wall_clock() {
656 let wait = |value: &str| parse(&[("retry-after", value)], clocks());
657 assert_eq!(
659 wait("Wed, 21 Oct 2015 07:28:30 GMT"),
660 Some(Duration::from_secs(30))
661 );
662 assert_eq!(wait("Wed, 21 Oct 2015 07:28:00 GMT"), Some(Duration::ZERO));
663 assert_eq!(wait("Wed, 21 Oct 2015 07:00:00 GMT"), Some(Duration::ZERO));
664 assert_eq!(
666 wait("Wednesday, 21-Oct-15 07:29:00 GMT"),
667 Some(Duration::from_secs(60))
668 );
669 assert_eq!(
670 wait("Wed Oct 21 07:28:05 2015"),
671 Some(Duration::from_secs(5))
672 );
673 }
674
675 #[tokio::test(start_paused = true)]
676 async fn seconds_count_from_receipt_and_dates_from_now() {
677 let mut clocks = clocks();
678 clocks.now = clocks.received_at + Duration::from_secs(2); assert_eq!(
680 parse(&[("retry-after", "5")], clocks),
681 Some(Duration::from_secs(3))
682 );
683 assert_eq!(
684 parse(&[("retry-after-ms", "1000")], clocks),
685 Some(Duration::ZERO)
686 );
687 assert_eq!(
688 parse(&[("retry-after", "Wed, 21 Oct 2015 07:28:05 GMT")], clocks),
689 Some(Duration::from_secs(5))
690 );
691 }
692
693 #[tokio::test(start_paused = true)]
694 async fn backs_off_and_caps() {
695 let mut policy = RetryPolicy::new(10)
696 .base_delay(Duration::from_millis(100))
697 .max_delay(Duration::from_secs(1))
698 .jitter(false);
699 let now = Instant::now();
700 assert_eq!(
701 policy.deadline(None, now),
702 Some(now + Duration::from_millis(100))
703 );
704 policy.attempts = 2;
705 assert_eq!(
706 policy.deadline(None, now),
707 Some(now + Duration::from_millis(400))
708 );
709 policy.attempts = 8;
710 assert_eq!(
711 policy.deadline(None, now),
712 Some(now + Duration::from_secs(1))
713 );
714 }
715
716 #[tokio::test(start_paused = true)]
717 async fn honours_retry_after_beyond_max_delay_up_to_max_retry_after() {
718 let policy = RetryPolicy::new(3).max_delay(Duration::from_secs(1));
719 let now = Instant::now();
720 let after = |secs| Some(now + Duration::from_secs(secs));
721 assert_eq!(policy.deadline(after(30), now), after(30));
722 assert_eq!(policy.deadline(after(60), now), after(60));
723 assert_eq!(policy.deadline(after(61), now), None);
724 assert_eq!(
726 policy.deadline(Some(now), now + Duration::from_secs(1)),
727 Some(now)
728 );
729 let policy = policy.max_retry_after(Duration::from_secs(5));
730 assert_eq!(policy.deadline(after(6), now), None);
731 }
732
733 #[tokio::test(start_paused = true)]
734 async fn jitter_stays_between_half_and_all_of_the_backoff() {
735 let backoff = Duration::from_millis(800);
736 assert_eq!(jittered(backoff, 0.0), Duration::from_millis(400));
737 assert_eq!(jittered(backoff, 0.5), Duration::from_millis(600));
738 assert!(jittered(backoff, 0.999_999) <= backoff);
739
740 let mut policy = RetryPolicy::new(10)
741 .base_delay(Duration::from_millis(100))
742 .max_delay(Duration::from_secs(1));
743 policy.attempts = 3; let delays: Vec<_> = (0..200).map(|_| policy.backoff()).collect();
745 for delay in &delays {
746 assert!(
747 (Duration::from_millis(400)..=backoff).contains(delay),
748 "{delay:?}"
749 );
750 }
751 assert!(delays.iter().any(|delay| *delay != delays[0]), "random");
752 let now = Instant::now();
754 let deadline = Some(now + Duration::from_secs(2));
755 assert_eq!(policy.deadline(deadline, now), deadline);
756 }
757
758 #[test]
759 fn random_unit_is_in_range() {
760 for _ in 0..1000 {
761 assert!((0.0..1.0).contains(&random_unit()));
762 }
763 }
764
765 #[test]
766 fn recognises_openai_insufficient_quota() {
767 assert!(openai_insufficient_quota(
768 br#"{"error":{"message":"You exceeded your current quota","type":"insufficient_quota","param":null,"code":"insufficient_quota"}}"#
769 ));
770 assert!(openai_insufficient_quota(
771 br#"{"error":{"type":"insufficient_quota"}}"#
772 ));
773 assert!(openai_insufficient_quota(
774 br#"{"error":{"type":"requests","code":"insufficient_quota"}}"#
775 ));
776 assert!(!openai_insufficient_quota(
777 br#"{"error":{"message":"Rate limit reached","type":"requests","code":"rate_limit_exceeded"}}"#
778 ));
779 assert!(!openai_insufficient_quota(b"insufficient_quota"));
780 assert!(!openai_insufficient_quota(b""));
781 }
782
783 #[test]
784 fn only_bodies_in_memory_are_cloned() {
785 let mut policy = RetryPolicy::default();
786 let mut clone_request = |request: &http::Request<Body>| {
787 Policy::<_, http::Response<()>, BoxError>::clone_request(&mut policy, request)
788 };
789 let request = http::Request::post("https://example.com")
790 .header("x", "1")
791 .body(Body::from("a"))
792 .unwrap();
793 let clone = clone_request(&request).unwrap();
794 assert_eq!(clone.uri(), request.uri());
795 assert_eq!(clone.headers()["x"], "1");
796 let chunks =
797 futures_util::stream::iter(vec![Ok::<_, std::io::Error>(bytes::Bytes::from("a"))]);
798 let streamed = http::Request::post("https://example.com")
799 .body(Body::wrap_stream(chunks))
800 .unwrap();
801 assert!(clone_request(&streamed).is_none());
802 }
803
804 fn body_429(chunks: Vec<Result<&'static str, &'static str>>) -> http::Response<Body> {
805 let stream = futures_util::stream::iter(
806 chunks
807 .into_iter()
808 .map(|chunk| chunk.map(Bytes::from).map_err(BoxError::from)),
809 );
810 http::Response::builder()
811 .status(429)
812 .body(Body::wrap_stream(stream))
813 .unwrap()
814 }
815
816 #[tokio::test]
817 async fn classifies_the_whole_body_and_puts_it_back() {
818 let response = body_429(vec![
819 Ok(r#"{"error":{"type":"#),
820 Ok(r#""insufficient_quota"}}"#),
821 ]);
822 let response = classify_429(response, &openai_insufficient_quota, 1024).await;
823 assert!(response.extensions().get::<Permanent>().is_some());
824 let body = response.into_body().collect_bytes().await.unwrap();
825 assert_eq!(body, r#"{"error":{"type":"insufficient_quota"}}"#);
826 }
827
828 #[tokio::test]
829 async fn a_body_over_the_limit_is_not_classified_but_kept_whole() {
830 let response = body_429(vec![Ok("0123"), Ok("4567"), Ok("89")]);
831 let response = classify_429(response, &|_: &[u8]| true, 5).await;
832 assert!(response.extensions().get::<Permanent>().is_none());
833 let body = response.into_body().collect_bytes().await.unwrap();
834 assert_eq!(body, "0123456789");
835 }
836
837 #[tokio::test]
838 async fn a_body_that_fails_is_not_classified_and_keeps_its_error() {
839 let response = body_429(vec![Ok("01"), Err("reset")]);
840 let response = classify_429(response, &|_: &[u8]| true, 1024).await;
841 assert!(response.extensions().get::<Permanent>().is_none());
842 let error = response.into_body().collect_bytes().await.unwrap_err();
843 assert_eq!(error.to_string(), "reset");
844 }
845}