Skip to main content

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}