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