Repository: sagebind/isahc
Pull request #417 (2022-10-13 05:20:43)

Remove several features from futures-lite
Disable default features in `futures-lite` which require several dependencies that are actually unnecessary for the very few things we use from it. Re-implement these few features instead.

codecov[bot]: # [Codecov](https://codecov.io/gh/sagebind/isahc/pull/417?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley) Report
Base: **52.47**% // Head: **52.65**% // Increases project coverage by **`+0.17%`** :tada:
> Coverage data is based on head [(`5857535`)](https://codecov.io/gh/sagebind/isahc/pull/417?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley) compared to base [(`6917903`)](https://codecov.io/gh/sagebind/isahc/commit/691790327b3341f7b7b99166864f15f99843a84d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley).
> Patch coverage: 75.67% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #417      +/-   ##
==========================================
+ Coverage   52.47%   52.65%   +0.17%     
==========================================
  Files          53       56       +3     
  Lines        5795     5846      +51     
==========================================
+ Hits         3041     3078      +37     
- Misses       2754     2768      +14     
```


| [Impacted Files](https://codecov.io/gh/sagebind/isahc/pull/417?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley) | Coverage Δ | |
|---|---|---|
| [src/handler.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL2hhbmRsZXIucnM=) | `80.82% <ø> (ø)` | |
| [src/lib.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL2xpYi5ycw==) | `17.24% <ø> (+0.55%)` | :arrow_up: |
| [src/text.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL3RleHQucnM=) | `81.39% <0.00%> (ø)` | |
| [src/util/mod.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL3V0aWwvbW9kLnJz) | `0.00% <ø> (ø)` | |
| [tests/request\_body.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-dGVzdHMvcmVxdWVzdF9ib2R5LnJz) | `100.00% <ø> (ø)` | |
| [src/util/io.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL3V0aWwvaW8ucnM=) | `68.75% <68.75%> (ø)` | |
| [src/util/future.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL3V0aWwvZnV0dXJlLnJz) | `69.56% <69.56%> (ø)` | |
| [src/agent/mod.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL2FnZW50L21vZC5ycw==) | `77.77% <100.00%> (ø)` | |
| [src/body/mod.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL2JvZHkvbW9kLnJz) | `83.78% <100.00%> (+0.29%)` | :arrow_up: |
| [src/body/sync.rs](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley#diff-c3JjL2JvZHkvc3luYy5ycw==) | `81.37% <100.00%> (ø)` | |
| ... and [8 more](https://codecov.io/gh/sagebind/isahc/pull/417/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/sagebind/isahc/pull/417?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Stephen+M.+Coakley).


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 1368303..d797d20 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,11 +35,11 @@ text-decoding = ["encoding_rs", "mime"]
 unstable-interceptors = []
 
 [dependencies]
-async-channel = "1.4.2" # channel used for internal communication
+async-channel = "1.7" # channel used for internal communication
 castaway = "0.2" # buffer type specialization
 crossbeam-utils = ">=0.7.0, <0.9.0" # synchronization primitives
 event-listener = "2.3.3" # synchronization primitive
-futures-lite = "1.10.1" # futures-io compatibility plus other helpers
+futures-io = "0.3.24" # futures-io ecosystem compatibility
 http = "0.2.1" # http ecosystem compatibility, part of API
 log = "0.4" # log ecosystem compatibility
 once_cell = "1" # used for a few singletons
@@ -68,6 +68,11 @@ optional = true
 version = "0.8"
 optional = true
 
+# common futures primitives
+[dependencies.futures-lite]
+version = "1.10.1"
+default-features = false
+
 # used in cookie parsing
 [dependencies.httpdate]
 version = "1"
@@ -113,6 +118,7 @@ features = ["std", "std-future"]
 
 [dev-dependencies]
 env_logger = "0.9"
+futures-lite = "1.10.1"
 flate2 = "1.0.3"
 indicatif = "0.15"
 rayon = "1"
diff --git a/examples/async.rs b/examples/async.rs
index c999b0c..4939d3b 100644
--- a/examples/async.rs
+++ b/examples/async.rs
@@ -1,10 +1,11 @@
 //! This example demonstrates the use of `send_async()` to make a request
 //! asynchronously using the futures-based API.
 
+use futures_lite::future::block_on;
 use isahc::prelude::*;
 
 fn main() -> Result<(), isahc::Error> {
-    futures_lite::future::block_on(async {
+    block_on(async {
         let mut response = isahc::get_async("http://example.org").await?;
 
         println!("Status: {}", response.status());
diff --git a/src/agent/mod.rs b/src/agent/mod.rs
index d3ceee9..10dcc8a 100644
--- a/src/agent/mod.rs
+++ b/src/agent/mod.rs
@@ -8,11 +8,10 @@
 //! Since request executions are driven through futures, the agent also acts as
 //! a specialized task executor for tasks related to requests.
 
-use crate::{error::Error, handler::RequestHandler, task::WakerExt};
+use crate::{error::Error, handler::RequestHandler, util::task::WakerExt};
 use async_channel::{Receiver, Sender};
 use crossbeam_utils::{atomic::AtomicCell, sync::WaitGroup};
 use curl::multi::{Events, Multi, Socket, SocketEvents};
-use futures_lite::future::block_on;
 use std::{
     collections::HashMap,
     io,
@@ -392,7 +391,7 @@ impl AgentContext {
     fn poll_messages(&mut self) -> Result<(), Error> {
         while !self.close_requested {
             if self.requests.is_empty() {
-                match block_on(self.message_rx.recv()) {
+                match self.message_rx.recv_blocking() {
                     Ok(message) => self.handle_message(message)?,
                     _ => {
                         tracing::warn!("agent handle disconnected without close message");
diff --git a/src/body/mod.rs b/src/body/mod.rs
index 35fe137..d0f5928 100644
--- a/src/body/mod.rs
+++ b/src/body/mod.rs
@@ -1,6 +1,7 @@
 //! Provides types for working with request and response bodies.
 
-use futures_lite::io::{AsyncRead, BlockOn};
+use crate::util::{future::FutureExt, io::read_async};
+use futures_io::AsyncRead;
 use std::{
     borrow::Cow,
     fmt,
@@ -174,13 +175,21 @@ impl AsyncBody {
     /// generally if the underlying reader only supports blocking under a
     /// specific runtime.
     pub(crate) fn into_sync(self) -> sync::Body {
+        struct ReadSyncAdapter<T>(T);
+
+        impl<T: AsyncRead + Unpin> Read for ReadSyncAdapter<T> {
+            fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+                read_async(&mut self.0, buf).wait()
+            }
+        }
+
         match self.0 {
             Inner::Empty => sync::Body::empty(),
             Inner::Buffer(cursor) => sync::Body::from_bytes_static(cursor.into_inner()),
             Inner::Reader(reader, Some(len)) => {
-                sync::Body::from_reader_sized(BlockOn::new(reader), len)
+                sync::Body::from_reader_sized(ReadSyncAdapter(reader), len)
             }
-            Inner::Reader(reader, None) => sync::Body::from_reader(BlockOn::new(reader)),
+            Inner::Reader(reader, None) => sync::Body::from_reader(ReadSyncAdapter(reader)),
         }
     }
 }
@@ -258,7 +267,7 @@ mod tests {
     use super::*;
     use futures_lite::{
         future::{block_on, zip},
-        io::AsyncReadExt,
+        io::{empty, AsyncReadExt},
     };
 
     static_assertions::assert_impl_all!(AsyncBody: Send, Sync);
@@ -281,7 +290,7 @@ mod tests {
 
     #[test]
     fn reader_with_unknown_length() {
-        let body = AsyncBody::from_reader(futures_lite::io::empty());
+        let body = AsyncBody::from_reader(empty());
 
         assert!(!body.is_empty());
         assert_eq!(body.len(), None);
@@ -289,7 +298,7 @@ mod tests {
 
     #[test]
     fn reader_with_known_length() {
-        let body = AsyncBody::from_reader_sized(futures_lite::io::empty(), 0);
+        let body = AsyncBody::from_reader_sized(empty(), 0);
 
         assert!(!body.is_empty());
         assert_eq!(body.len(), Some(0));
diff --git a/src/body/sync.rs b/src/body/sync.rs
index 73e1abe..2ead308 100644
--- a/src/body/sync.rs
+++ b/src/body/sync.rs
@@ -1,5 +1,6 @@
 use super::AsyncBody;
-use futures_lite::{future::yield_now, io::AsyncWriteExt};
+use crate::util::io::write_all_async;
+use futures_lite::future::yield_now;
 use sluice::pipe::{pipe, PipeWriter};
 use std::{
     borrow::Cow,
@@ -275,7 +276,7 @@ impl Writer {
                 Err(e) => return Err(e),
             };
 
-            self.writer.write_all(&buf[..len]).await?;
+            write_all_async(&mut self.writer, &buf[..len]).await?;
         }
     }
 }
diff --git a/src/client.rs b/src/client.rs
index e7894b3..6361dc7 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -14,11 +14,10 @@ use crate::{
     headers::HasHeaders,
     interceptor::{self, Interceptor, InterceptorObj},
     parsing::header_to_curl_string,
+    util::future::FutureExt,
 };
-use futures_lite::{
-    future::{block_on, try_zip},
-    io::AsyncRead,
-};
+use futures_io::AsyncRead;
+use futures_lite::future::try_zip;
 use http::{
     header::{HeaderMap, HeaderName, HeaderValue},
     Request,
@@ -924,28 +923,27 @@ impl HttpClient {
             async_body
         });
 
-        let response = block_on(
-            async move {
-                // Instead of simply blocking the current thread until the response
-                // is received, we can use the current thread to read from the
-                // request body synchronously while concurrently waiting for the
-                // response.
-                if let Some(mut writer) = writer_maybe {
-                    // Note that the `send_async` future is given first; this
-                    // ensures that it is polled first and thus the request is
-                    // initiated before we attempt to write the request body.
-                    let (response, _) = try_zip(self.send_async_inner(request), async move {
-                        writer.write().await.map_err(Error::from)
-                    })
-                    .await?;
-
-                    Ok(response)
-                } else {
-                    self.send_async_inner(request).await
-                }
+        let response = async move {
+            // Instead of simply blocking the current thread until the response
+            // is received, we can use the current thread to read from the
+            // request body synchronously while concurrently waiting for the
+            // response.
+            if let Some(mut writer) = writer_maybe {
+                // Note that the `send_async` future is given first; this
+                // ensures that it is polled first and thus the request is
+                // initiated before we attempt to write the request body.
+                let (response, _) = try_zip(self.send_async_inner(request), async move {
+                    writer.write().await.map_err(Error::from)
+                })
+                .await?;
+
+                Ok(response)
+            } else {
+                self.send_async_inner(request).await
             }
-            .instrument(span),
-        )?;
+        }
+        .instrument(span)
+        .wait()?;
 
         Ok(response.map(|body| body.into_sync()))
     }
diff --git a/src/handler.rs b/src/handler.rs
index 9a21df8..f1abe25 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -11,7 +11,7 @@ use crate::{
 use async_channel::Sender;
 use curl::easy::{Easy2, InfoType, ReadError, SeekResult, WriteError};
 use curl_sys::CURL;
-use futures_lite::io::{AsyncRead, AsyncWrite};
+use futures_io::{AsyncRead, AsyncWrite};
 use http::Response;
 use once_cell::sync::OnceCell;
 use sluice::pipe;
diff --git a/src/lib.rs b/src/lib.rs
index e9813c9..cfc4066 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -261,9 +261,9 @@ mod parsing;
 mod redirect;
 mod request;
 mod response;
-mod task;
 mod text;
 mod trailer;
+mod util;
 
 pub mod auth;
 pub mod config;
diff --git a/src/response.rs b/src/response.rs
index e2dfd18..06d526e 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -1,5 +1,10 @@
-use crate::{metrics::Metrics, redirect::EffectiveUri, trailer::Trailer};
-use futures_lite::io::{copy as copy_async, AsyncRead, AsyncWrite};
+use crate::{
+    metrics::Metrics,
+    redirect::EffectiveUri,
+    trailer::Trailer,
+    util::io::{copy_async, Sink},
+};
+use futures_io::{AsyncRead, AsyncWrite};
 use http::{Response, Uri};
 use std::{
     fs::File,
@@ -452,7 +457,7 @@ pub trait AsyncReadResponseExt<R: AsyncRead + Unpin> {
 impl<R: AsyncRead + Unpin> AsyncReadResponseExt<R> for Response<R> {
     fn consume(&mut self) -> ConsumeFuture<'_, R> {
         ConsumeFuture::new(async move {
-            copy_async(self.body_mut(), futures_lite::io::sink()).await?;
+            copy_async(self.body_mut(), Sink).await?;
 
             Ok(())
         })
diff --git a/src/text.rs b/src/text.rs
index 946b463..7f63c30 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -2,9 +2,9 @@
 
 #![cfg(feature = "text-decoding")]
 
-use crate::headers::HasHeaders;
+use crate::{headers::HasHeaders, util::io::read_async};
 use encoding_rs::{CoderResult, Encoding};
-use futures_lite::io::{AsyncRead, AsyncReadExt};
+use futures_io::AsyncRead;
 use http::Response;
 use std::io;
 
@@ -85,7 +85,7 @@ impl Decoder {
     where
         R: AsyncRead + Unpin + 'r,
     {
-        TextFuture::new(async move { decode_reader!(self, buf, reader.read(buf).await) })
+        TextFuture::new(async move { decode_reader!(self, buf, read_async(&mut reader, buf).await) })
     }
 
     /// Push additional bytes into the decoder, returning any trailing bytes
diff --git a/src/util/future.rs b/src/util/future.rs
new file mode 100644
index 0000000..8854271
--- /dev/null
+++ b/src/util/future.rs
@@ -0,0 +1,51 @@
+//! Tiny module containing utilities for working with futures.
+
+use super::MaybeOwned;
+use crossbeam_utils::sync::Parker;
+use futures_lite::pin;
+use std::{
+    cell::RefCell,
+    future::Future,
+    task::{Context, Poll, Waker},
+};
+use waker_fn::waker_fn;
+
+pub(crate) trait FutureExt: Future {
+    /// Block the current thread until this future completes.
+    fn wait(self) -> <Self as Future>::Output
+    where
+        Self: Sized,
+    {
+        fn create_parker() -> (Parker, Waker) {
+            let parker = Parker::new();
+            let unparker = parker.unparker().clone();
+            let waker = waker_fn(move || unparker.unpark());
+
+            (parker, waker)
+        }
+
+        thread_local! {
+            static PARKER: RefCell<(Parker, Waker)> = RefCell::new(create_parker());
+        }
+
+        PARKER.with(|cell| {
+            let future = self;
+            pin!(future);
+
+            let (ref parker, ref waker) = *cell
+                .try_borrow_mut()
+                .map(MaybeOwned::Borrowed)
+                .unwrap_or_else(|_| MaybeOwned::Owned(create_parker()));
+            let mut cx = Context::from_waker(waker);
+
+            loop {
+                match future.as_mut().poll(&mut cx) {
+                    Poll::Ready(result) => break result,
+                    Poll::Pending => parker.park(),
+                }
+            }
+        })
+    }
+}
+
+impl<F: Future> FutureExt for F {}
diff --git a/src/util/io.rs b/src/util/io.rs
new file mode 100644
index 0000000..a1b3f1d
--- /dev/null
+++ b/src/util/io.rs
@@ -0,0 +1,87 @@
+//! Very tiny module containing helpers for `AsyncRead` and `AsyncWrite`.
+
+use futures_io::{AsyncRead, AsyncWrite};
+use futures_lite::{
+    future::{poll_fn, yield_now},
+    pin,
+};
+use std::{
+    io::{ErrorKind, Result},
+    pin::Pin,
+    task::{Context, Poll},
+};
+
+pub(crate) async fn read_async<R>(reader: &mut R, buf: &mut [u8]) -> Result<usize>
+where
+    R: AsyncRead + Unpin,
+{
+    pin!(reader);
+
+    poll_fn(|cx| reader.as_mut().poll_read(cx, buf)).await
+}
+
+pub(crate) async fn write_async<R>(writer: &mut R, buf: &[u8]) -> Result<usize>
+where
+    R: AsyncWrite + Unpin,
+{
+    pin!(writer);
+
+    poll_fn(|cx| writer.as_mut().poll_write(cx, buf)).await
+}
+
+pub(crate) async fn write_all_async<R>(writer: &mut R, buf: &[u8]) -> Result<usize>
+where
+    R: AsyncWrite + Unpin,
+{
+    let mut amt = 0;
+
+    while amt < buf.len() {
+        match write_async(writer, &buf[amt..]).await {
+            Ok(0) => return Err(ErrorKind::WriteZero.into()),
+            Ok(len) => amt += len,
+            Err(e) if e.kind() == ErrorKind::Interrupted => yield_now().await,
+            Err(e) => return Err(e),
+        }
+    }
+
+    Ok(amt)
+}
+
+pub(crate) async fn copy_async<R, W>(mut reader: R, mut writer: W) -> Result<u64>
+where
+    R: AsyncRead + Unpin,
+    W: AsyncWrite + Unpin,
+{
+    let mut buf = [0; 8192];
+
+    let mut amt = 0;
+
+    loop {
+        match read_async(&mut reader, &mut buf).await {
+            Ok(0) => break,
+            Ok(len) => {
+                amt += write_all_async(&mut writer, &buf[..len]).await? as u64;
+            }
+            Err(e) if e.kind() == ErrorKind::Interrupted => yield_now().await,
+            Err(e) => return Err(e),
+        }
+    }
+
+    Ok(amt)
+}
+
+pub(crate) struct Sink;
+
+impl AsyncWrite for Sink {
+    fn poll_write(self: Pin<&mut Self>, _cx: &mut Context, buf: &[u8]) -> Poll<Result<usize>> {
+        Poll::Ready(Ok(buf.len()))
+    }
+
+    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Result<()>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn poll_close(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Result<()>> {
+        Poll::Ready(Ok(()))
+    }
+}
diff --git a/src/util/mod.rs b/src/util/mod.rs
new file mode 100644
index 0000000..6833ef7
--- /dev/null
+++ b/src/util/mod.rs
@@ -0,0 +1,24 @@
+use std::ops::Deref;
+
+pub(crate) mod future;
+pub(crate) mod io;
+pub(crate) mod task;
+
+enum MaybeOwned<T, B> {
+    Owned(T),
+    Borrowed(B),
+}
+
+impl<T, B> Deref for MaybeOwned<T, B>
+where
+    B: Deref<Target = T>,
+{
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            Self::Owned(ref value) => value,
+            Self::Borrowed(borrow) => borrow.deref(),
+        }
+    }
+}
diff --git a/src/task.rs b/src/util/task.rs
similarity index 80%
rename from src/task.rs
rename to src/util/task.rs
index 49a759a..36f9731 100644
--- a/src/task.rs
+++ b/src/util/task.rs
@@ -1,6 +1,7 @@
-//! Helpers for working with tasks and futures.
+//! Helpers for working with tasks.
 
 use std::task::Waker;
+use waker_fn::waker_fn;
 
 /// Helper methods for working with wakers.
 pub(crate) trait WakerExt {
@@ -12,6 +13,6 @@ pub(crate) trait WakerExt {
 impl WakerExt for Waker {
     fn chain(&self, f: impl Fn(&Waker) + Send + Sync + 'static) -> Waker {
         let inner = self.clone();
-        waker_fn::waker_fn(move || (f)(&inner))
+        waker_fn(move || (f)(&inner))
     }
 }


Test patch:
diff --git a/tests/json.rs b/tests/json.rs
index d0dbf4f..72dbfbc 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -1,6 +1,7 @@
 #![cfg(feature = "json")]
 
-use futures_lite::{future::block_on, io::AsyncRead};
+use futures_io::AsyncRead;
+use futures_lite::future::block_on;
 use isahc::prelude::*;
 use serde_json::Value;
 use std::{
diff --git a/tests/request_body.rs b/tests/request_body.rs
index 26e2c2f..0342bff 100644
--- a/tests/request_body.rs
+++ b/tests/request_body.rs
@@ -1,4 +1,5 @@
-use futures_lite::{future::block_on, AsyncRead};
+use futures_io::AsyncRead;
+use futures_lite::future::block_on;
 use isahc::{prelude::*, AsyncBody, Body, Request};
 use std::{
     error::Error,