1use std::borrow::Cow;
2use std::fmt;
3use std::ops::{Bound, RangeBounds};
4use std::sync::{Arc, Mutex, PoisonError};
5
6use bytes::{Bytes, BytesMut};
7use futures_core::Stream;
8use futures_util::{future, stream, StreamExt, TryStreamExt};
9
10use crate::maybe::BoxStream;
11use crate::{Body, BoxError, Error, MaybeSend, MaybeSync};
12
13type Chunks = BoxStream<'static, Result<Bytes, BoxError>>;
14type Once = Arc<Mutex<Option<Chunks>>>;
15#[cfg(not(target_family = "wasm"))]
16type Factory = Arc<dyn Fn() -> Chunks + Send + Sync>;
17#[cfg(target_family = "wasm")]
18type Factory = Arc<dyn Fn() -> Chunks>;
19
20#[cfg_attr(
30 all(feature = "fs", not(target_family = "wasm")),
31 doc = "| [`Upload::path`], [`Upload::file`] | from the file's metadata | yes: the file is opened again |"
32)]
33#[cfg_attr(
34 not(all(feature = "fs", not(target_family = "wasm"))),
35 doc = "| `Upload::path`, `Upload::file` (feature `fs`, not on WebAssembly) | from the file's metadata | yes: the file is opened again |"
36)]
37#[cfg_attr(
39 all(feature = "fs", not(target_family = "wasm")),
40 doc = "| [`Upload::reader`], [`Upload::stream`] | [`Upload::len`] | no, unless [`Upload::buffer_for_retry`] holds it all |"
41)]
42#[cfg_attr(
43 not(all(feature = "fs", not(target_family = "wasm"))),
44 doc = "| `Upload::reader` (feature `fs`), [`Upload::stream`] | [`Upload::len`] | no, unless [`Upload::buffer_for_retry`] holds it all |"
45)]
46#[cfg_attr(
49 all(feature = "fs", not(target_family = "wasm")),
50 doc = "A path's length is read by [`Upload::resolve`], which generated clients call; [`Upload::file`] is"
51)]
52#[cfg_attr(
53 not(all(feature = "fs", not(target_family = "wasm"))),
54 doc = "A path's length is read by [`Upload::resolve`], which generated clients call; `Upload::file` is"
55)]
56#[derive(Clone)]
73pub struct Upload {
74 source: Source,
75 pub(crate) file_name: Option<Cow<'static, str>>,
76 pub(crate) mime: Option<Cow<'static, str>>,
77 total: Option<u64>,
79 start: u64,
80 end: Option<u64>,
81 buffer_limit: Option<usize>,
82}
83
84#[derive(Clone)]
85enum Source {
86 Bytes(Bytes),
87 #[cfg(feature = "fs")]
88 File(Arc<std::path::Path>),
89 Once(Once),
90 Factory(Factory),
91}
92
93impl Upload {
94 pub fn bytes(bytes: impl Into<Bytes>) -> Self {
96 let bytes = bytes.into();
97 let total = Some(bytes.len() as u64);
98 Self::new(Source::Bytes(bytes), total)
99 }
100
101 pub fn stream<S, B, E>(stream: S) -> Self
105 where
106 S: Stream<Item = Result<B, E>> + MaybeSend + 'static,
107 B: Into<Bytes> + 'static,
108 E: Into<BoxError> + 'static,
109 {
110 let chunks: Chunks = Box::pin(stream.map_ok(Into::into).map_err(Into::into));
111 Self::new(Source::Once(Arc::new(Mutex::new(Some(chunks)))), None)
112 }
113
114 pub fn from_fn<F, S, B, E>(make: F) -> Self
118 where
119 F: Fn() -> S + MaybeSend + MaybeSync + 'static,
120 S: Stream<Item = Result<B, E>> + MaybeSend + 'static,
121 B: Into<Bytes> + 'static,
122 E: Into<BoxError> + 'static,
123 {
124 let factory: Factory =
125 Arc::new(move || Box::pin(make().map_ok(Into::into).map_err(Into::into)));
126 Self::new(Source::Factory(factory), None)
127 }
128
129 fn new(source: Source, total: Option<u64>) -> Self {
130 Self {
131 source,
132 file_name: None,
133 mime: None,
134 total,
135 start: 0,
136 end: None,
137 buffer_limit: None,
138 }
139 }
140
141 pub fn file_name(mut self, file_name: impl Into<Cow<'static, str>>) -> Self {
144 self.file_name = Some(file_name.into());
145 self
146 }
147
148 pub fn mime(mut self, mime: impl Into<Cow<'static, str>>) -> Self {
151 self.mime = Some(mime.into());
152 self
153 }
154
155 pub fn name(&self) -> Option<&str> {
158 self.file_name.as_deref()
159 }
160
161 pub fn media_type(&self) -> Option<&str> {
163 self.mime.as_deref()
164 }
165
166 pub fn len(mut self, len: u64) -> Self {
169 self.total = Some(len);
170 self
171 }
172
173 pub fn slice(mut self, range: impl RangeBounds<u64>) -> Self {
177 let from = match range.start_bound() {
178 Bound::Included(&start) => start,
179 Bound::Excluded(&start) => start.saturating_add(1),
180 Bound::Unbounded => 0,
181 };
182 let to = match range.end_bound() {
183 Bound::Included(&end) => Some(end.saturating_add(1)),
184 Bound::Excluded(&end) => Some(end),
185 Bound::Unbounded => None,
186 };
187 let start = self.start.saturating_add(from);
188 let end = to.map(|to| self.start.saturating_add(to));
189 self.end = match (self.end, end) {
190 (Some(old), Some(new)) => Some(old.min(new)),
191 (old, new) => old.or(new),
192 };
193 self.start = match self.end {
194 Some(end) => start.min(end),
195 None => start,
196 };
197 self
198 }
199
200 pub fn buffer_for_retry(mut self, limit: usize) -> Self {
204 self.buffer_limit = Some(limit);
205 self
206 }
207
208 pub fn content_length(&self) -> Option<u64> {
211 self.total.map(|total| {
212 let end = self.end.unwrap_or(total).min(total);
213 end.saturating_sub(self.start.min(total))
214 })
215 }
216
217 pub fn is_replayable(&self) -> bool {
220 !matches!(self.source, Source::Once(_))
221 }
222
223 pub async fn resolve(mut self) -> Result<Self, Error> {
227 #[cfg(all(feature = "fs", target_family = "wasm"))]
228 if let Source::File(path) = &self.source {
229 return Err(Error::io(no_file_system(path)));
230 }
231 #[cfg(all(feature = "fs", not(target_family = "wasm")))]
232 if let Source::File(path) = &self.source {
233 if self.total.is_none() {
234 let metadata = tokio::fs::metadata(&**path)
235 .await
236 .map_err(|error| Error::io(with_path(error, path)))?;
237 if !metadata.is_file() {
238 return Err(Error::io(std::io::Error::other(format!(
239 "{}: not a file",
240 path.display()
241 ))));
242 }
243 self.total = Some(metadata.len());
244 }
245 return Ok(self);
246 }
247 let Some(limit) = self.buffer_limit else {
248 return Ok(self);
249 };
250 let Source::Once(once) = &self.source else {
251 return Ok(self);
252 };
253 let taken = once.lock().unwrap_or_else(PoisonError::into_inner).take();
254 let Some(mut chunks) = taken else {
255 return Ok(self);
256 };
257 let mut buffer = BytesMut::new();
258 while let Some(chunk) = chunks.next().await {
259 let chunk = chunk.map_err(Error::io)?;
260 if buffer.len() + chunk.len() > limit {
261 let read = stream::iter([Ok(buffer.freeze()), Ok(chunk)]).chain(chunks);
263 let chunks: Chunks = Box::pin(read);
264 self.source = Source::Once(Arc::new(Mutex::new(Some(chunks))));
265 return Ok(self);
266 }
267 buffer.extend_from_slice(&chunk);
268 }
269 self.total = Some(buffer.len() as u64);
270 self.source = Source::Bytes(buffer.freeze());
271 Ok(self)
272 }
273}
274
275#[cfg(feature = "fs")]
276impl Upload {
277 #[cfg_attr(
281 not(target_family = "wasm"),
282 doc = "until the request is sent; [`Upload::resolve`] (or [`Upload::file`]) reads the length, so the body is sent with"
283 )]
284 #[cfg_attr(
285 target_family = "wasm",
286 doc = "until the request is sent; [`Upload::resolve`] (or `Upload::file`) reads the length, so the body is sent with"
287 )]
288 pub fn path(path: impl Into<std::path::PathBuf>) -> Self {
294 let path: Arc<std::path::Path> = Arc::from(path.into());
295 let file_name = path
296 .file_name()
297 .map(|name| Cow::Owned(name.to_string_lossy().into_owned()));
298 let mime = mime_guess::from_path(&path)
299 .first()
300 .map(|mime| Cow::Owned(mime.essence_str().to_owned()));
301 let mut upload = Self::new(Source::File(path), None);
302 upload.file_name = file_name;
303 upload.mime = mime;
304 upload
305 }
306}
307
308#[cfg(all(feature = "fs", not(target_family = "wasm")))]
309impl Upload {
310 pub async fn file(path: impl Into<std::path::PathBuf>) -> Result<Self, Error> {
312 Self::path(path).resolve().await
313 }
314
315 pub fn reader<R>(reader: R) -> Self
318 where
319 R: tokio::io::AsyncRead + Send + 'static,
320 {
321 Self::stream(read_chunks(Box::pin(reader), None))
322 }
323}
324
325#[cfg(all(feature = "fs", not(target_family = "wasm")))]
327fn read_chunks<R>(
328 reader: R,
329 expected: Option<u64>,
330) -> impl Stream<Item = std::io::Result<Bytes>> + Send + 'static
331where
332 R: tokio::io::AsyncRead + Send + Unpin + 'static,
333{
334 use tokio::io::AsyncReadExt;
335 const CHUNK: usize = 64 * 1024;
336 stream::try_unfold((reader, 0u64), move |(mut reader, read)| async move {
337 let mut buffer = BytesMut::with_capacity(CHUNK);
338 let n = reader.read_buf(&mut buffer).await?;
339 if n == 0 {
340 return match expected {
341 Some(expected) if read < expected => Err(std::io::Error::new(
342 std::io::ErrorKind::UnexpectedEof,
343 format!("the upload ended after {read} of its {expected} bytes"),
344 )),
345 _ => Ok(None),
346 };
347 }
348 Ok(Some((buffer.freeze(), (reader, read + n as u64))))
349 })
350}
351
352#[cfg(all(feature = "fs", target_family = "wasm"))]
354fn no_file_system(path: &std::path::Path) -> std::io::Error {
355 std::io::Error::new(
356 std::io::ErrorKind::Unsupported,
357 format!(
358 "{}: no file system on WebAssembly: upload bytes or a stream instead",
359 path.display()
360 ),
361 )
362}
363
364#[cfg(all(feature = "fs", not(target_family = "wasm")))]
365fn with_path(error: std::io::Error, path: &std::path::Path) -> std::io::Error {
366 std::io::Error::new(error.kind(), format!("{}: {error}", path.display()))
367}
368
369#[cfg(all(feature = "fs", not(target_family = "wasm")))]
371fn file_chunks(
372 path: Arc<std::path::Path>,
373 start: u64,
374 end: Option<u64>,
375 len: Option<u64>,
376) -> Chunks {
377 use tokio::io::{AsyncReadExt, AsyncSeekExt};
378 let open = async move {
379 let mut file = tokio::fs::File::open(&*path)
380 .await
381 .map_err(|error| with_path(error, &path))?;
382 if start > 0 {
383 file.seek(std::io::SeekFrom::Start(start)).await?;
384 }
385 let limit = len
386 .or_else(|| end.map(|end| end.saturating_sub(start)))
387 .unwrap_or(u64::MAX);
388 Ok::<_, std::io::Error>(read_chunks(file.take(limit), len))
389 };
390 Box::pin(stream::once(open).try_flatten().map_err(BoxError::from))
391}
392
393fn range(chunks: Chunks, start: u64, end: Option<u64>) -> Chunks {
395 if start == 0 && end.is_none() {
396 return chunks;
397 }
398 Box::pin(stream::unfold(
399 (chunks, 0u64),
400 move |(mut chunks, mut position)| async move {
401 loop {
402 if end.is_some_and(|end| position >= end) {
403 return None;
404 }
405 let chunk = match chunks.next().await? {
406 Ok(chunk) => chunk,
407 Err(error) => return Some((Err(error), (chunks, position))),
408 };
409 let first = position;
410 position += chunk.len() as u64;
411 let from = start.max(first);
412 let to = end.map_or(position, |end| end.min(position));
413 if from < to {
414 let slice = chunk.slice((from - first) as usize..(to - first) as usize);
415 return Some((Ok(slice), (chunks, position)));
416 }
417 }
418 },
419 ))
420}
421
422fn take_once(once: Once) -> Chunks {
424 let taken = stream::once(future::lazy(move |_| {
425 once.lock().unwrap_or_else(PoisonError::into_inner).take()
426 }));
427 Box::pin(taken.flat_map(|chunks| match chunks {
428 Some(chunks) => chunks,
429 None => Box::pin(stream::once(future::ready(Err(BoxError::from(
430 "this upload is a stream that was sent already: a stream is read once",
431 ))))) as Chunks,
432 }))
433}
434
435impl From<Upload> for Body {
436 fn from(upload: Upload) -> Self {
437 let len = upload.content_length();
438 let (start, end) = (upload.start, upload.end);
439 match upload.source {
440 Source::Bytes(bytes) => {
441 let total = bytes.len() as u64;
442 let to = end.unwrap_or(total).min(total);
443 let from = start.min(to);
444 Body::from(bytes.slice(from as usize..to as usize))
445 }
446 #[cfg(all(feature = "fs", target_family = "wasm"))]
447 Source::File(path) => Body::wrap_stream(stream::once(future::ready(Err::<Bytes, _>(
448 no_file_system(&path),
449 )))),
450 #[cfg(all(feature = "fs", not(target_family = "wasm")))]
451 Source::File(path) => Body::replay(
452 Arc::new(move || {
453 Body::sized_stream(file_chunks(path.clone(), start, end, len), len)
454 }),
455 len,
456 ),
457 Source::Factory(make) => Body::replay(
458 Arc::new(move || Body::sized_stream(range(make(), start, end), len)),
459 len,
460 ),
461 Source::Once(once) => Body::sized_stream(range(take_once(once), start, end), len),
462 }
463 }
464}
465
466impl From<Bytes> for Upload {
467 fn from(bytes: Bytes) -> Self {
468 Self::bytes(bytes)
469 }
470}
471
472impl From<Vec<u8>> for Upload {
473 fn from(bytes: Vec<u8>) -> Self {
474 Self::bytes(bytes)
475 }
476}
477
478impl From<&'static [u8]> for Upload {
479 fn from(bytes: &'static [u8]) -> Self {
480 Self::bytes(Bytes::from_static(bytes))
481 }
482}
483
484impl From<&Upload> for Upload {
485 fn from(upload: &Upload) -> Self {
486 upload.clone()
487 }
488}
489
490#[cfg(feature = "fs")]
491impl From<std::path::PathBuf> for Upload {
492 fn from(path: std::path::PathBuf) -> Self {
493 Self::path(path)
494 }
495}
496
497#[cfg(feature = "fs")]
498impl From<&std::path::PathBuf> for Upload {
499 fn from(path: &std::path::PathBuf) -> Self {
500 Self::path(path.clone())
501 }
502}
503
504#[cfg(feature = "fs")]
505impl From<&std::path::Path> for Upload {
506 fn from(path: &std::path::Path) -> Self {
507 Self::path(path)
508 }
509}
510
511impl PartialEq for Upload {
519 fn eq(&self, other: &Self) -> bool {
520 let same_source = match (&self.source, &other.source) {
521 (Source::Bytes(a), Source::Bytes(b)) => a == b,
522 #[cfg(feature = "fs")]
523 (Source::File(a), Source::File(b)) => a == b,
524 (Source::Once(a), Source::Once(b)) => Arc::ptr_eq(a, b),
525 (Source::Factory(a), Source::Factory(b)) => {
526 std::ptr::addr_eq(Arc::as_ptr(a), Arc::as_ptr(b))
527 }
528 _ => false,
529 };
530 #[cfg(feature = "fs")]
531 let total = matches!(self.source, Source::File(_)) || self.total == other.total;
532 #[cfg(not(feature = "fs"))]
533 let total = self.total == other.total;
534 same_source
535 && total
536 && self.file_name == other.file_name
537 && self.mime == other.mime
538 && self.start == other.start
539 && self.end == other.end
540 && self.buffer_limit == other.buffer_limit
541 }
542}
543
544impl fmt::Debug for Upload {
545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546 let mut debug = f.debug_struct("Upload");
547 match &self.source {
548 Source::Bytes(bytes) => debug.field("bytes", &bytes.len()),
549 #[cfg(feature = "fs")]
550 Source::File(path) => debug.field("path", path),
551 Source::Once(_) => debug.field("stream", &true),
552 Source::Factory(_) => debug.field("from_fn", &true),
553 };
554 debug
555 .field("file_name", &self.file_name)
556 .field("mime", &self.mime)
557 .field("content_length", &self.content_length())
558 .finish_non_exhaustive()
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 async fn read(upload: Upload) -> Bytes {
567 Body::from(upload).collect_bytes().await.unwrap()
568 }
569
570 fn chunks(
571 parts: &[&'static str],
572 ) -> impl Stream<Item = Result<Bytes, std::io::Error>> + Send + 'static {
573 stream::iter(
574 parts
575 .iter()
576 .map(|part| Ok(Bytes::from_static(part.as_bytes())))
577 .collect::<Vec<_>>(),
578 )
579 }
580
581 #[test]
582 fn uploads_are_equal_when_they_send_the_same_thing() {
583 let bytes = Upload::bytes(&b"abc"[..]).file_name("a.txt");
584 assert_eq!(bytes, bytes.clone());
585 assert_eq!(bytes, Upload::bytes(b"abc".to_vec()).file_name("a.txt"));
586 assert_ne!(bytes, Upload::bytes(&b"abc"[..]));
587 assert_ne!(bytes, bytes.clone().mime("text/plain"));
588 assert_ne!(bytes, bytes.clone().slice(1..));
589 let stream = Upload::stream(stream::iter([Ok::<_, std::io::Error>(&b"abc"[..])]));
590 assert_eq!(stream, stream.clone());
591 assert_ne!(
592 stream,
593 Upload::stream(stream::iter([Ok::<_, std::io::Error>(&b"abc"[..])]))
594 );
595 assert_ne!(stream, Upload::bytes(&b"abc"[..]));
596 let from_fn = Upload::from_fn(|| stream::iter([Ok::<_, std::io::Error>(&b"abc"[..])]));
597 assert_eq!(from_fn, from_fn.clone());
598 assert_ne!(from_fn, stream);
599 }
600
601 #[cfg(feature = "fs")]
602 #[tokio::test]
603 async fn paths_are_equal_as_paths() {
604 let file = tempfile::NamedTempFile::new().unwrap();
605 let path = Upload::path(file.path());
606 assert_eq!(path, Upload::from(file.path().to_path_buf()));
607 assert_eq!(path, path.clone().resolve().await.unwrap());
608 assert_ne!(path, Upload::path("other.txt"));
609 }
610
611 #[tokio::test]
612 async fn bytes_are_replayable_and_sliced_without_a_copy() {
613 let upload = Upload::bytes("hello world").slice(6..);
614 assert_eq!(upload.content_length(), Some(5));
615 let body = Body::from(upload);
616 assert_eq!(http_body::Body::size_hint(&body).exact(), Some(5));
617 let copy = body.try_clone().unwrap();
618 assert_eq!(body.collect_bytes().await.unwrap(), "world");
619 assert_eq!(copy.collect_bytes().await.unwrap(), "world");
620 assert_eq!(
621 read(Upload::bytes("hello").slice(1..=2).slice(1..)).await,
622 "l"
623 );
624 assert_eq!(read(Upload::bytes("hello").slice(10..)).await, "");
625 }
626
627 #[tokio::test]
628 async fn a_stream_is_read_once() {
629 let upload = Upload::stream(chunks(&["ab", "cd"]));
630 assert!(!upload.is_replayable());
631 let copy = upload.clone();
632 let body = Body::from(upload);
633 assert!(body.try_clone().is_none());
634 assert_eq!(body.collect_bytes().await.unwrap(), "abcd");
635 assert!(Body::from(copy).collect_bytes().await.is_err());
636 }
637
638 #[tokio::test]
639 async fn a_stream_is_sliced_across_chunks() {
640 assert_eq!(
641 read(Upload::stream(chunks(&["ab", "cd", "ef"])).slice(1..5)).await,
642 "bcde"
643 );
644 let sized = Upload::stream(chunks(&["ab", "cd"])).len(4).slice(1..);
645 assert_eq!(sized.content_length(), Some(3));
646 assert_eq!(
647 http_body::Body::size_hint(&Body::from(sized)).exact(),
648 Some(3)
649 );
650 }
651
652 #[tokio::test]
653 async fn from_fn_is_replayed() {
654 let upload = Upload::from_fn(|| chunks(&["x", "y"])).len(2);
655 assert!(upload.is_replayable());
656 let body = Body::from(upload);
657 let copy = body.try_clone().unwrap();
658 assert_eq!(body.collect_bytes().await.unwrap(), "xy");
659 assert_eq!(copy.collect_bytes().await.unwrap(), "xy");
660 }
661
662 #[tokio::test]
663 async fn buffer_for_retry_holds_a_short_stream() {
664 let short = Upload::stream(chunks(&["ab", "cd"]))
665 .buffer_for_retry(4)
666 .resolve()
667 .await
668 .unwrap();
669 assert!(short.is_replayable());
670 assert_eq!(short.content_length(), Some(4));
671 assert_eq!(read(short).await, "abcd");
672
673 let long = Upload::stream(chunks(&["ab", "cd", "e"]))
674 .buffer_for_retry(4)
675 .resolve()
676 .await
677 .unwrap();
678 assert!(!long.is_replayable());
679 assert_eq!(long.content_length(), None);
680 assert_eq!(read(long).await, "abcde");
681 }
682
683 #[cfg(all(feature = "fs", not(target_family = "wasm")))]
684 #[tokio::test]
685 async fn a_file_is_resolved_reopened_and_sliced() {
686 let dir = tempfile::tempdir().unwrap();
687 let path = dir.path().join("notes.json");
688 std::fs::write(&path, "0123456789").unwrap();
689
690 let upload = Upload::path(&path);
691 assert_eq!(upload.file_name.as_deref(), Some("notes.json"));
692 assert_eq!(upload.mime.as_deref(), Some("application/json"));
693 assert_eq!(upload.content_length(), None);
694 let upload = upload.resolve().await.unwrap();
695 assert_eq!(upload.content_length(), Some(10));
696
697 let body = Body::from(upload.clone());
698 assert_eq!(http_body::Body::size_hint(&body).exact(), Some(10));
699 let copy = body.try_clone().unwrap();
700 assert_eq!(body.collect_bytes().await.unwrap(), "0123456789");
701 assert_eq!(copy.collect_bytes().await.unwrap(), "0123456789");
702
703 let slice = upload.slice(2..5);
704 assert_eq!(slice.content_length(), Some(3));
705 assert_eq!(read(slice.clone()).await, "234");
706 assert_eq!(read(slice.slice(1..)).await, "34");
707 let tail = Upload::file(&path).await.unwrap().slice(8..20);
708 assert_eq!(tail.content_length(), Some(2));
709 assert_eq!(read(tail).await, "89");
710 }
711
712 #[cfg(all(feature = "fs", not(target_family = "wasm")))]
713 #[tokio::test]
714 async fn a_missing_file_fails_to_resolve() {
715 let error = Upload::file("/no/such/file.bin").await.unwrap_err();
716 assert!(error.is_io(), "{error:?}");
717 assert!(error.to_string().contains("/no/such/file.bin"), "{error}");
718 }
719
720 #[cfg(all(feature = "fs", not(target_family = "wasm")))]
721 #[tokio::test]
722 async fn a_file_that_shrank_is_an_error() {
723 let dir = tempfile::tempdir().unwrap();
724 let path = dir.path().join("data.bin");
725 std::fs::write(&path, "0123456789").unwrap();
726 let upload = Upload::file(&path).await.unwrap();
727 std::fs::write(&path, "0123").unwrap();
728 assert!(Body::from(upload).collect_bytes().await.is_err());
729 }
730
731 #[cfg(all(feature = "fs", not(target_family = "wasm")))]
732 #[tokio::test]
733 async fn a_reader_is_read_once() {
734 let upload = Upload::reader(&b"from a reader"[..]);
735 assert!(!upload.is_replayable());
736 assert_eq!(read(upload).await, "from a reader");
737 }
738}