hclient_core/body.rs
1use bytes::Bytes;
2use std::fmt::Debug;
3use std::sync::Arc;
4
5/// Whether this body can be replayed — known **before** sending.
6///
7/// `reqwest::Request::try_clone() -> Option<Request>` answers the same
8/// question after the retry layer has already decided to retry, and so
9/// silently disables retries on streaming bodies.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum RetryKind {
12 /// Replays for free.
13 Free,
14 /// Replays by calling the factory.
15 ViaFactory,
16 /// Cannot be replayed.
17 Impossible,
18}
19
20/// `Send + Sync` bounds — a documented exception to the crate invariant
21/// "declare `Send`/`Sync` nowhere" (spec amendment-C2, sibling of C1 on
22/// [`crate::Error`]). Without them `RequestBody` would be `!Send`, so
23/// `http::Request<RequestBody>` would be `!Send`, so the future
24/// `Transport::execute` returns would be `!Send` for every backend —
25/// `tokio::spawn(client.get(u).send())` would never build. `Sync` is only
26/// needed here, for `Arc`: `Arc<T>: Send` requires `T: Send + Sync`, whereas
27/// `Box<T>: Send` (see [`RequestBody::Streaming`]) requires only `T: Send`.
28pub type RewindFactory = Arc<dyn Fn() -> RequestBody + Send + Sync>; // send-bound-exception: amendment-C2
29
30/// A request body with an explicit replay contract.
31#[derive(Default)]
32pub enum RequestBody {
33 #[default]
34 Empty,
35 Full(Bytes),
36 /// Replays by calling the factory.
37 ///
38 /// **Factory contract.** It must be pure: every call must produce a body
39 /// equivalent to the previous one (same content, same size). A factory
40 /// with hidden state that hands back a different body on each call is a
41 /// bug source that's obvious in hindsight but undocumented otherwise,
42 /// which is why `size_hint()` deliberately returns `None` for this
43 /// variant: guessing from the first call is dangerous if the contract
44 /// is violated.
45 ///
46 /// The factory is legally allowed to return `RequestBody::Streaming` —
47 /// that isn't a live lie, because `retry_kind()` and `rewind()` are
48 /// always recomputed from whatever object currently sits inside
49 /// `RequestBody`, not cached at the moment `Rewindable` was created.
50 /// **Invariant that matters for the retry layer: always ask
51 /// `retry_kind()` of the body you're currently holding, and never cache
52 /// it across a `rewind()`.**
53 Rewindable(RewindFactory),
54 /// A single-pass body. The concrete stream is set by the transport; in
55 /// v0.1 the core only needs to know it can't be replayed.
56 ///
57 /// `+ Send` — the same C2 exception as [`RewindFactory`]: `Box<T>: Send`
58 /// requires only `T: Send`, `Sync` isn't needed here.
59 Streaming(Box<dyn http_body::Body<Data = Bytes, Error = crate::Error> + Unpin + Send>), // send-bound-exception: amendment-C2
60}
61
62impl Debug for RequestBody {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 RequestBody::Empty => f.write_str("Empty"),
66 RequestBody::Full(b) => write!(f, "Full({} bytes)", b.len()),
67 RequestBody::Rewindable(_) => f.write_str("Rewindable(..)"),
68 RequestBody::Streaming(_) => f.write_str("Streaming(..)"),
69 }
70 }
71}
72
73impl RequestBody {
74 pub fn rewindable<F>(f: F) -> Self
75 where
76 F: Fn() -> RequestBody + Send + Sync + 'static, // send-bound-exception: amendment-C2
77 {
78 RequestBody::Rewindable(Arc::new(f))
79 }
80
81 pub fn retry_kind(&self) -> RetryKind {
82 match self {
83 RequestBody::Empty | RequestBody::Full(_) => RetryKind::Free,
84 RequestBody::Rewindable(_) => RetryKind::ViaFactory,
85 RequestBody::Streaming(_) => RetryKind::Impossible,
86 }
87 }
88
89 pub fn rewind(&self) -> Option<RequestBody> {
90 match self {
91 RequestBody::Empty => Some(RequestBody::Empty),
92 RequestBody::Full(b) => Some(RequestBody::Full(b.clone())),
93 RequestBody::Rewindable(f) => Some(f()),
94 RequestBody::Streaming(_) => None,
95 }
96 }
97
98 pub fn size_hint(&self) -> Option<u64> {
99 match self {
100 RequestBody::Empty => Some(0),
101 RequestBody::Full(b) => Some(b.len() as u64),
102 _ => None,
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use bytes::Bytes;
111 use std::pin::Pin;
112 use std::task::Context;
113 use std::task::Poll;
114
115 #[test]
116 fn replayability_is_knowable_before_sending() {
117 assert_eq!(RequestBody::Empty.retry_kind(), RetryKind::Free);
118 assert_eq!(
119 RequestBody::Full(Bytes::from_static(b"x")).retry_kind(),
120 RetryKind::Free
121 );
122 }
123
124 #[test]
125 fn rewindable_replays_through_factory() {
126 let b = RequestBody::rewindable(|| RequestBody::Full(Bytes::from_static(b"same")));
127 assert_eq!(b.retry_kind(), RetryKind::ViaFactory);
128 let again = b.rewind().expect("rewindable must rewind");
129 assert!(matches!(again, RequestBody::Full(ref x) if &x[..] == b"same"));
130 }
131
132 #[test]
133 fn full_rewind_preserves_the_payload() {
134 let b = RequestBody::Full(Bytes::from_static(b"abc"));
135 match b.rewind().expect("Full replays") {
136 RequestBody::Full(x) => assert_eq!(&x[..], b"abc"),
137 other => panic!("expected Full, got {other:?}"),
138 }
139 }
140
141 #[test]
142 fn a_factory_survives_repeated_replays() {
143 use std::sync::atomic::{AtomicUsize, Ordering};
144 let calls = Arc::new(AtomicUsize::new(0));
145 let c = calls.clone();
146 let b = RequestBody::rewindable(move || {
147 c.fetch_add(1, Ordering::SeqCst);
148 RequestBody::Full(Bytes::from_static(b"same"))
149 });
150 for _ in 0..3 {
151 let again = b.rewind().expect("rewindable replays");
152 assert!(matches!(again, RequestBody::Full(ref x) if &x[..] == b"same"));
153 assert_eq!(
154 b.retry_kind(),
155 RetryKind::ViaFactory,
156 "kind doesn't change across replays"
157 );
158 }
159 assert_eq!(calls.load(Ordering::SeqCst), 3);
160 }
161
162 /// The `Empty`/`Full` pair are the only variants whose size is known
163 /// ahead of time. `Rewindable` and `Streaming` are covered separately
164 /// (`rewindable_replays_through_factory`,
165 /// `streaming_is_honest_about_being_unreplayable`) and aren't included
166 /// here — the test's name shouldn't promise coverage it doesn't have.
167 #[test]
168 fn size_hint_is_known_for_empty_and_full_bodies() {
169 assert_eq!(RequestBody::Empty.size_hint(), Some(0));
170 assert_eq!(
171 RequestBody::Full(Bytes::from_static(b"abcd")).size_hint(),
172 Some(4)
173 );
174 }
175
176 /// A body with not a single byte in its buffer: `poll_frame` returns
177 /// `Ready(None)` immediately. Needed only to construct
178 /// `RequestBody::Streaming` in tests — the concrete transport supplies
179 /// its own implementation.
180 struct EmptyStream;
181 impl http_body::Body for EmptyStream {
182 type Data = Bytes;
183 type Error = crate::Error;
184 fn poll_frame(
185 self: Pin<&mut Self>,
186 _: &mut Context<'_>,
187 ) -> Poll<Option<Result<http_body::Frame<Bytes>, Self::Error>>> {
188 Poll::Ready(None)
189 }
190 }
191
192 #[test]
193 fn streaming_is_honest_about_being_unreplayable() {
194 let b = RequestBody::Streaming(Box::new(EmptyStream));
195 assert_eq!(b.retry_kind(), RetryKind::Impossible);
196 assert!(b.rewind().is_none(), "must return None, not panic");
197 assert_eq!(b.size_hint(), None);
198 }
199
200 // `RequestBody: Send` and `http::Request<RequestBody>: Send`
201 // (amendment-C2) are asserted in `crates/hclient-core/tests/shape.rs`,
202 // for `error.rs`'s reason: a bare `fn assert_send<T: Send>() {}` inside
203 // `src` is what the `no-declared-send` guard's regex matches, and an
204 // assertion outside `src` needs no exception marker at all.
205}