Repository: seanmonstar/reqwest
Pull request #2732 (2025-06-14 10:17:11)

fix: Fix SOCKS4a proxy protocol matching
I believe it was originally SOCKS4a rather than SOCKS4h — perhaps this was changed during the refactor?

https://www.openssh.com/txt/socks4a.protocol


Patch:
diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs
index 13e4c33..936bdd4 100644
--- a/src/async_impl/client.rs
+++ b/src/async_impl/client.rs
@@ -3020,7 +3020,7 @@ impl Future for PendingRequest {
                             }
                         }
 
-                        return Poll::Ready(Err(e));
+                        return Poll::Ready(Err(e.if_no_url(|| self.url.clone())));
                     }
                     Poll::Ready(Ok(res)) => res.map(super::body::boxed),
                     Poll::Pending => return Poll::Pending,
diff --git a/src/async_impl/multipart.rs b/src/async_impl/multipart.rs
index 5933e8a..a51b5ce 100644
--- a/src/async_impl/multipart.rs
+++ b/src/async_impl/multipart.rs
@@ -402,7 +402,7 @@ impl<P: PartProps> FormParts<P> {
     }
 
     // If predictable, computes the length the request will have
-    // The length should be preditable if only String and file fields have been added,
+    // The length should be predictable if only String and file fields have been added,
     // but not if a generic reader has been added;
     pub(crate) fn compute_length(&mut self) -> Option<u64> {
         let mut length = 0u64;
diff --git a/src/connect.rs b/src/connect.rs
index 0a84d4c..76deb58 100644
--- a/src/connect.rs
+++ b/src/connect.rs
@@ -492,7 +492,7 @@ impl ConnectorService {
     async fn connect_socks(mut self, dst: Uri, proxy: Intercepted) -> Result<Conn, BoxError> {
         let dns = match proxy.uri().scheme_str() {
             Some("socks4") | Some("socks5") => socks::DnsResolve::Local,
-            Some("socks4h") | Some("socks5h") => socks::DnsResolve::Proxy,
+            Some("socks4a") | Some("socks5h") => socks::DnsResolve::Proxy,
             _ => {
                 unreachable!("connect_socks is only called for socks proxies");
             }
@@ -653,7 +653,7 @@ impl ConnectorService {
 
         #[cfg(feature = "socks")]
         match proxy.uri().scheme_str().ok_or("proxy scheme expected")? {
-            "socks4" | "socks4h" | "socks5" | "socks5h" => {
+            "socks4" | "socks4a" | "socks5" | "socks5h" => {
                 return self.connect_socks(dst, proxy).await
             }
             _ => (),
@@ -1300,7 +1300,7 @@ mod socks {
 
         // TODO: can `Scheme::from_static()` be const fn, compare with a SOCKS5 constant?
         match proxy.uri().scheme_str() {
-            Some("socks4") | Some("socks4h") => {
+            Some("socks4") | Some("socks4a") => {
                 let mut svc = SocksV4::new(proxy_uri, http_connector);
                 let stream = Service::call(&mut svc, dst_uri)
                     .await
diff --git a/src/dns/resolve.rs b/src/dns/resolve.rs
index b9ccf78..b04ff00 100644
--- a/src/dns/resolve.rs
+++ b/src/dns/resolve.rs
@@ -83,7 +83,7 @@ impl DynResolver {
             .port_u16()
             .unwrap_or_else(|| match target.scheme_str() {
                 Some("https") => 443,
-                Some("socks4") | Some("socks4h") | Some("socks5") | Some("socks5h") => 1080,
+                Some("socks4") | Some("socks4a") | Some("socks5") | Some("socks5h") => 1080,
                 _ => 80,
             });
 
diff --git a/src/error.rs b/src/error.rs
index d67d5ce..0a4d563 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -76,6 +76,13 @@ impl Error {
         self
     }
 
+    pub(crate) fn if_no_url(mut self, f: impl FnOnce() -> Url) -> Self {
+        if self.inner.url.is_none() {
+            self.inner.url = Some(f());
+        }
+        self
+    }
+
     /// Strip the related url from this error (if, for example, it contains
     /// sensitive information)
     pub fn without_url(mut self) -> Self {
@@ -113,6 +120,12 @@ impl Error {
             if err.is::<TimedOut>() {
                 return true;
             }
+            #[cfg(not(target_arch = "wasm32"))]
+            if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
+                if hyper_err.is_timeout() {
+                    return true;
+                }
+            }
             if let Some(io) = err.downcast_ref::<io::Error>() {
                 if io.kind() == io::ErrorKind::TimedOut {
                     return true;
@@ -446,7 +459,9 @@ mod tests {
         let err = super::request(super::TimedOut);
         assert!(err.is_timeout());
 
-        let io = io::Error::new(io::ErrorKind::Other, err);
+        // todo: test `hyper::Error::is_timeout` when we can easily construct one
+
+        let io = io::Error::from(io::ErrorKind::TimedOut);
         let nested = super::request(io);
         assert!(nested.is_timeout());
     }
diff --git a/src/wasm/body.rs b/src/wasm/body.rs
index 241aa81..8d40c50 100644
--- a/src/wasm/body.rs
+++ b/src/wasm/body.rs
@@ -182,6 +182,14 @@ impl fmt::Debug for Body {
     }
 }
 
+impl Default for Body {
+    fn default() -> Body {
+        Body {
+            inner: Inner::Single(Single::Bytes(Bytes::new())),
+        }
+    }
+}
+
 // Can use new methods in web-sys when requiring v0.2.93.
 // > `init.method(m)` to `init.set_method(m)`
 // For now, ignore their deprecation.


Test patch:
diff --git a/tests/client.rs b/tests/client.rs
index 3ae438e..307892c 100644
--- a/tests/client.rs
+++ b/tests/client.rs
@@ -593,3 +593,10 @@ async fn http1_reason_phrase() {
         )
     );
 }
+
+#[tokio::test]
+async fn error_has_url() {
+    let u = "http://does.not.exist.local/ever";
+    let err = reqwest::get(u).await.unwrap_err();
+    assert_eq!(err.url().map(AsRef::as_ref), Some(u), "{err:?}");
+}
diff --git a/tests/redirect.rs b/tests/redirect.rs
index 8facb83..1867173 100644
--- a/tests/redirect.rs
+++ b/tests/redirect.rs
@@ -403,7 +403,7 @@ async fn test_redirect_limit_to_1() {
         .build()
         .unwrap();
     let res = client.get(&url).send().await.unwrap_err();
-    // If the maxmium limit is 1, then the final uri should be /redirect/1
+    // If the maximum limit is 1, then the final uri should be /redirect/1
     assert_eq!(
         res.url().unwrap().as_str(),
         format!("http://{}/redirect/1", server.addr()).as_str()