reqwest_streams/stream_body.rs
1//! Streaming a request body.
2//!
3//! The mirror of this crate's response side: instead of decoding a body into a stream of
4//! items, this encodes a stream of items into a body you can `POST` or `PUT`.
5
6use crate::observability::{ReqwestStreamErrorHandler, ReqwestStreamProgress, ReqwestStreamProgressHandler};
7use crate::{StreamBodyError, StreamBodyResult};
8use bytes::Bytes;
9use futures::stream::BoxStream;
10use futures::{Stream, StreamExt};
11use http_streams_core::format::{StreamFormat, StreamFormatEncode};
12use http_streams_core::{
13 buffer_bytes, buffer_ready_items, count_bytes, count_items, encode_stream, instrument,
14 Counting, Direction, Progress, ProgressOptions, Side, StreamContext, StreamErrorKind,
15};
16use reqwest::header::HeaderValue;
17use std::sync::Arc;
18use std::time::Duration;
19
20/// Options for a streamed request body.
21///
22/// Separate from [`ReqwestStreamOptions`] on purpose: that one carries `max_obj_len` and
23/// `buf_capacity`, which are decode-side concepts — a guard against a hostile peer, and the
24/// size of a read buffer. Neither means anything when you are the one producing the bytes.
25///
26/// [`ReqwestStreamOptions`]: crate::ReqwestStreamOptions
27#[non_exhaustive]
28pub struct ReqwestStreamBodyOptions {
29 /// Overrides the `Content-Type` the format would otherwise set.
30 pub content_type: Option<HeaderValue>,
31 /// Coalesce output into chunks of at least this many bytes.
32 pub buffering_bytes: Option<usize>,
33 /// Coalesce every N items that are ready together into one chunk.
34 pub buffering_ready_items: Option<usize>,
35 /// Invoked for every error produced while encoding the body.
36 pub on_error: Option<ReqwestStreamErrorHandler>,
37 /// Invoked for every progress report.
38 pub on_progress: Option<ReqwestStreamProgressHandler>,
39 /// How often to report interim progress. One second by default.
40 pub progress_interval: Option<Duration>,
41 /// Additionally report progress every N items.
42 pub progress_items: Option<u64>,
43}
44
45impl Default for ReqwestStreamBodyOptions {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl ReqwestStreamBodyOptions {
52 /// Default options.
53 pub fn new() -> Self {
54 Self {
55 content_type: None,
56 buffering_bytes: None,
57 buffering_ready_items: None,
58 on_error: None,
59 on_progress: None,
60 progress_interval: Some(http_streams_core::DEFAULT_PROGRESS_INTERVAL),
61 progress_items: None,
62 }
63 }
64
65 /// Sets the `Content-Type`.
66 ///
67 /// This is the **only** reliable way to override it, because
68 /// [`RequestBuilder::header`] *appends* rather than replaces:
69 ///
70 /// - Chaining `.header(CONTENT_TYPE, …)` **after** one of the `*_stream_body` methods sends
71 /// two `Content-Type` headers.
72 /// - Chaining it **once before** is replaced, which is what you would want.
73 /// - Chaining it **twice before** leaves the second value in place, because replacement
74 /// overwrites only the first value for a name. The request then carries two after all.
75 ///
76 /// [`RequestBuilder::header`]: reqwest::RequestBuilder::header
77 pub fn content_type(mut self, content_type: HeaderValue) -> Self {
78 self.content_type = Some(content_type);
79 self
80 }
81
82 /// Coalesce output into chunks of at least `size` bytes.
83 ///
84 /// Worth setting for formats whose items are small: without it, JSON Lines of short
85 /// objects emits one chunked-transfer frame per item.
86 ///
87 /// Ignored if [`buffering_ready_items`](Self::buffering_ready_items) is also set; the two
88 /// are alternatives and the item-based one wins.
89 pub fn buffering_bytes(mut self, size: usize) -> Self {
90 self.buffering_bytes = Some(size);
91 self
92 }
93
94 /// Coalesce every `count` items that are ready together into one chunk.
95 ///
96 /// Takes precedence over [`buffering_bytes`](Self::buffering_bytes) if both are set.
97 pub fn buffering_ready_items(mut self, count: usize) -> Self {
98 self.buffering_ready_items = Some(count);
99 self
100 }
101
102 /// Registers a callback invoked for every error produced while encoding the body.
103 ///
104 /// Worth more here than on the response side. When a request body errors, hyper aborts the
105 /// request and [`send`] returns a generic transport error with the original cause usually
106 /// flattened away — so this is often the only way to find out *what* failed.
107 ///
108 /// [`send`]: reqwest::RequestBuilder::send
109 pub fn on_error<F>(mut self, handler: F) -> Self
110 where
111 F: Fn(&StreamBodyError) + Send + Sync + 'static,
112 {
113 self.on_error = Some(Arc::new(handler));
114 self
115 }
116
117 /// Registers a callback receiving progress snapshots as the body is uploaded.
118 pub fn on_progress<F>(mut self, handler: F) -> Self
119 where
120 F: Fn(&ReqwestStreamProgress) + Send + Sync + 'static,
121 {
122 self.on_progress = Some(Arc::new(handler));
123 self
124 }
125
126 /// Reports progress at most once per `interval`.
127 pub fn progress_interval(mut self, interval: Duration) -> Self {
128 self.progress_interval = Some(interval);
129 self
130 }
131
132 /// Additionally reports progress every `items` items.
133 pub fn progress_items(mut self, items: u64) -> Self {
134 self.progress_items = Some(items);
135 self
136 }
137
138 fn progress_options(&self) -> ProgressOptions {
139 let mut opts = ProgressOptions::new();
140 opts.on_error = self.on_error.clone();
141 opts.on_progress = self.on_progress.clone();
142 opts.progress_interval = self.progress_interval;
143 opts.progress_items = self.progress_items;
144 opts
145 }
146}
147
148/// A request body that streams a sequence of items.
149///
150/// Convert it into a [`reqwest::Body`] with `.into()`, or hand it to
151/// [`StreamBodyRequest::stream_body`], which also sets the `Content-Type`.
152///
153/// # HTTP caveats
154///
155/// Streaming a *request* body is much less universally supported than streaming a response.
156/// None of the following stops it working, but each will surprise you if it is not expected.
157///
158/// 1. **The body cannot be replayed.** [`RequestBuilder::try_clone`] returns `None` for a
159/// streaming body, so retry middleware — `reqwest-retry` and anything like it — cannot
160/// retry the request.
161/// 2. **A redirect silently sends an empty body.** `reqwest` follows redirects through a
162/// middleware that substitutes a default body when the original cannot be cloned, and for
163/// `reqwest` that default is an *empty* body. A 307 or 308 on a streaming upload therefore
164/// arrives at the new location with nothing in it, and no error is reported. **Use
165/// [`redirect::Policy::none`] for streaming uploads** and handle redirects yourself.
166/// 3. **Transfer-Encoding is chunked.** No `Content-Length` can be computed, so HTTP/1.1 uses
167/// chunked encoding. Some API gateways reject chunked request bodies. HTTP/2 is unaffected.
168/// 4. **`Expect: 100-continue` is not supported.** hyper neither sends it nor waits for it, so
169/// setting the header by hand does not get you the behaviour: you may upload a great many
170/// bytes before learning the request was rejected. When the server *does* answer early, the
171/// body is dropped and the outcome is reported as `aborted`.
172/// 5. **Buffering reverse proxies defeat streaming.** nginx buffers request bodies by default
173/// (`proxy_request_buffering on`), as do many CDNs and API gateways; the server then sees
174/// one complete body rather than a stream. Set `proxy_request_buffering off;`.
175/// 6. **Timeouts cover the whole exchange.** [`RequestBuilder::timeout`] spans connect through
176/// response body, so a slow *source* stream can trip it.
177///
178/// [`RequestBuilder::try_clone`]: reqwest::RequestBuilder::try_clone
179/// [`RequestBuilder::timeout`]: reqwest::RequestBuilder::timeout
180/// [`redirect::Policy::none`]: reqwest::redirect::Policy::none
181pub struct ReqwestStreamBody {
182 stream: BoxStream<'static, StreamBodyResult<Bytes>>,
183 content_type: HeaderValue,
184}
185
186impl std::fmt::Debug for ReqwestStreamBody {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("ReqwestStreamBody")
189 .field("content_type", &self.content_type)
190 .finish_non_exhaustive()
191 }
192}
193
194impl ReqwestStreamBody {
195 /// A body encoding `stream` with `format`.
196 pub fn new<S, T, FMT>(format: FMT, stream: S) -> Self
197 where
198 FMT: StreamFormatEncode<T> + StreamFormat,
199 FMT::Encoder: Send + 'static,
200 S: Stream<Item = T> + Send + 'static,
201 T: Send + 'static,
202 {
203 Self::with_options(format, stream, ReqwestStreamBodyOptions::new())
204 }
205
206 /// A body encoding a fallible `stream` with `format`.
207 ///
208 /// Errors from your source stream are forwarded into the body stream, where they abort the
209 /// request. Use [`ReqwestStreamBodyOptions::on_error`] to observe them.
210 pub fn try_new<S, T, FMT, E>(format: FMT, stream: S) -> Self
211 where
212 FMT: StreamFormatEncode<T> + StreamFormat,
213 FMT::Encoder: Send + 'static,
214 S: Stream<Item = Result<T, E>> + Send + 'static,
215 T: Send + 'static,
216 E: Into<Box<dyn std::error::Error + Send + Sync>> + Send + 'static,
217 {
218 Self::try_with_options(format, stream, ReqwestStreamBodyOptions::new())
219 }
220
221 /// A body encoding `stream` with `format`, with options.
222 pub fn with_options<S, T, FMT>(
223 format: FMT,
224 stream: S,
225 options: ReqwestStreamBodyOptions,
226 ) -> Self
227 where
228 FMT: StreamFormatEncode<T> + StreamFormat,
229 FMT::Encoder: Send + 'static,
230 S: Stream<Item = T> + Send + 'static,
231 T: Send + 'static,
232 {
233 Self::build(format, stream.map(Ok), options)
234 }
235
236 /// A body encoding a fallible `stream` with `format`, with options.
237 pub fn try_with_options<S, T, FMT, E>(
238 format: FMT,
239 stream: S,
240 options: ReqwestStreamBodyOptions,
241 ) -> Self
242 where
243 FMT: StreamFormatEncode<T> + StreamFormat,
244 FMT::Encoder: Send + 'static,
245 S: Stream<Item = Result<T, E>> + Send + 'static,
246 T: Send + 'static,
247 E: Into<Box<dyn std::error::Error + Send + Sync>> + Send + 'static,
248 {
249 let normalised = stream.map(|item| {
250 item.map_err(|err| {
251 StreamBodyError::new(StreamErrorKind::InputOutputError, Some(err.into()), None)
252 })
253 });
254 Self::build(format, normalised, options)
255 }
256
257 fn build<S, T, FMT>(format: FMT, stream: S, options: ReqwestStreamBodyOptions) -> Self
258 where
259 FMT: StreamFormatEncode<T> + StreamFormat,
260 FMT::Encoder: Send + 'static,
261 S: Stream<Item = StreamBodyResult<T>> + Send + 'static,
262 T: Send + 'static,
263 {
264 let content_type = options.content_type.clone().unwrap_or_else(|| {
265 HeaderValue::from_static(format.default_content_type())
266 });
267
268 let context = StreamContext::new(format.format_name(), Direction::Request, Side::Client)
269 .content_type(content_type.to_str().unwrap_or_default());
270 let context = match options.buffering_bytes {
271 Some(bytes) => context.buf_capacity(bytes),
272 None => context,
273 };
274 let progress = Progress::new(&context, &options.progress_options());
275
276 // Items only exist as items upstream of the encoder, so this is where to count them.
277 let items = Box::pin(count_items(stream, &progress));
278 let bytes = encode_stream(items, format.encoder());
279
280 let buffered: BoxStream<'static, StreamBodyResult<Bytes>> =
281 match (options.buffering_ready_items, options.buffering_bytes) {
282 (Some(count), _) => Box::pin(buffer_ready_items(bytes, count)),
283 (_, Some(size)) => Box::pin(buffer_bytes(bytes, size)),
284 (None, None) => Box::pin(bytes),
285 };
286
287 let counted = count_bytes(buffered, &progress);
288
289 // Outermost, so its `Drop` coincides with the body's — which is how an upload the
290 // server cut short (an early 401 or 413, a dropped connection, a timeout) is noticed.
291 // `Counting::Bytes` because by here the items are chunks, counted above as items.
292 let stream = Box::pin(instrument(Box::pin(counted), progress, Counting::Bytes));
293
294 Self {
295 stream,
296 content_type,
297 }
298 }
299
300 /// The `Content-Type` this body should be sent with.
301 ///
302 /// [`StreamBodyRequest::stream_body`] and the per-format methods set it for you; this is
303 /// for callers building a request by hand.
304 pub fn content_type(&self) -> &HeaderValue {
305 &self.content_type
306 }
307
308 /// The encoded bytes, for callers who are not sending an HTTP request.
309 ///
310 /// Public on purpose: it makes the body testable without a server, and lets you write the
311 /// same encoding to a file, a socket, or an object-store SDK.
312 pub fn into_stream(self) -> BoxStream<'static, StreamBodyResult<Bytes>> {
313 self.stream
314 }
315}
316
317impl From<ReqwestStreamBody> for reqwest::Body {
318 fn from(body: ReqwestStreamBody) -> Self {
319 reqwest::Body::wrap_stream(body.stream)
320 }
321}
322
323/// Sets a streamed body and its `Content-Type` on a request in one step.
324pub trait StreamBodyRequest {
325 /// Sets `body` as the request body, along with its `Content-Type`.
326 fn stream_body(self, body: ReqwestStreamBody) -> reqwest::RequestBuilder;
327}
328
329impl StreamBodyRequest for reqwest::RequestBuilder {
330 fn stream_body(self, body: ReqwestStreamBody) -> reqwest::RequestBuilder {
331 // `headers`, not `header`: the latter *appends*, so a caller who had already set a
332 // Content-Type would end up sending two. `headers` goes through replace semantics.
333 let mut headers = reqwest::header::HeaderMap::with_capacity(1);
334 headers.insert(reqwest::header::CONTENT_TYPE, body.content_type().clone());
335 self.headers(headers).body(reqwest::Body::from(body))
336 }
337}