Guide

How to Use Proxies With Rust (reqwest and ureq, measured)

Proxy code for reqwest 0.13 and ureq 3: Proxy::all, basic_auth, SOCKS5 with the socks feature, NO_PROXY, rotation per connection and every error text, measured.

HProxy Team··Updated September 2, 2026·21 min read
HProxy.Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.44/GB, pay as you go.

Proxies for Web Scraping

On reqwest 0.13 you attach the proxy to the Client with Proxy::all, and every request that client sends goes through it. The whole program is eleven lines.

use reqwest::{Client, Proxy};

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = Client::builder()
        .proxy(Proxy::all("http://user:pass@host:port")?)
        .build()?;
    let r = client.get("https://httpbin.org/ip").send().await?;
    println!("{} {}", r.status(), r.text().await?);
    Ok(())
}

We ran exactly this code on 2 September 2026 with a public entry from our free list as the proxy address. The target answered with the address of the proxy, not with ours.

Our terminal running the reqwest program through a public proxy and printing 200 OK with the proxy address as the origin.
Captured on our own machine on 2 September 2026. The origin is the address of the proxy, so the proxy carried the request.

The rest of this page is the same code path, measured on reqwest 0.13.4 against local proxies with known credentials and against public entries. It covers authentication, the tunnel behind an https:// target, SOCKS5, the environment variables reqwest reads on its own, and rotation. It shows the exact error for each way the proxy path can fail, and it compares reqwest with ureq for synchronous code.

What you need before you start

  • Rust 1.85 or newer, because reqwest 0.13.4 lists 1.85 as its minimum on crates.io (May 2026). Check with rustc --version.
  • The crates. reqwest needs a tokio runtime for its async client, and SOCKS support sits behind the socks feature.
  • A proxy URL in the form scheme://user:pass@host:port. For a first test, any entry from our free proxy list will do.
[dependencies]
reqwest = { version = "0.13", features = ["socks"] }
tokio = { version = "1", features = ["full"] }

Add "blocking" to the features if your code is synchronous, and "json" if you want .json() on responses. The proxy API has grown over the years, and old examples on the web still show the state of 2019. The dates come from the reqwest changelog and the crates.io release history.

reqwest versionreleasedwhat changed for proxies
0.9.6January 2019Proxy::basic_auth added.
0.10.0December 2019Environment and system proxies on by default; the synchronous client moved to reqwest::blocking.
0.10.1January 2020The socks feature added.
0.11.14January 2023Proxy::no_proxy added.
0.12.6August 2024HTTP_PROXY and HTTPS_PROXY take precedence over ALL_PROXY.
0.12.8 and 0.12.13September 2024 and March 2025SOCKS4 and SOCKS4a added.
0.12.23August 2025SOCKS URLs without a port default to 1080.
0.12.25December 2025Proxy-Authorization is sent to HTTPS proxies for HTTP targets.
0.13.0December 2025rustls became the default TLS backend.
0.13.4May 2026The current release, and the one measured on this page.

How do I use a proxy with reqwest?

Step 1: build the Client with a Proxy

The proxy is a property of the Client, so build the client once and reuse it. Proxy::all returns a Result, because the URL is parsed at that point.

use reqwest::{Client, Proxy};
use std::time::Duration;

let client = Client::builder()
    .proxy(Proxy::all("http://host:8080")?.basic_auth("user", "pass"))
    .timeout(Duration::from_secs(15))
    .build()?;

Step 2: choose which requests the rule covers

Proxy::all covers http:// and https:// targets. Proxy::http and Proxy::https cover one scheme each, and a request that no rule matches goes direct. We proved the scoping with a dead proxy on a closed port.

constructormatchesmeasured on 0.13.4
Proxy::all(url)every http:// and https:// request.Wrong password on the proxy: every request fails.
Proxy::http(url)http:// requests only.Dead proxy plus an https:// target: 200, direct.
Proxy::https(url)https:// requests only.Dead proxy plus an http:// target: 200, direct.
Proxy::custom(closure)whatever the closure returns per target URL.Runs once per new connection, see the rotation section.

Step 3: keep the order in mind

The client checks the rules in the order they were added. The docs warn that an eager rule such as Proxy::all added first "would prevent a Proxy later in the list from ever working". We added Proxy::all with a wrong password and then Proxy::https with the right one. The request failed on the first rule.

The proxy rule is evaluated when a connection is opened, then the connection is reused
  1. Client

    Proxy rules, in order

  2. Connection pool

    one connection per host and proxy

  3. CONNECT or SOCKS

    opened through the proxy

  4. Target

    sees the exit IP

Source: Build the Client once; a pooled connection keeps the proxy it was opened with

How do I add a username and password?

Either chain .basic_auth onto the Proxy, or write the credentials into the URL. Both worked against a local proxy that requires Basic auth.

let proxy = Proxy::all("http://host:8080")?.basic_auth("user", "pass");
// or
let proxy = Proxy::all("http://user:pass@host:8080")?;

For an HTTP proxy, reqwest sends a Proxy-Authorization header with the Basic scheme of RFC 7617: the user id, a colon and the password, encoded in Base64. The basic_auth call writes the credentials into the proxy URL inside reqwest. That is why the same call also authenticates against a SOCKS5 proxy, which the SOCKS section shows.

Special characters in the password need one precaution when they sit in the URL. We tested them on 0.13.4.

password containsin the URL, rawwith .basic_auth()
@works, the parser splits at the last @.works.
/ or #Proxy::all fails: builder error <- invalid port number.works.
%40 for @works.not needed.

Percent-encode the password in a URL, or pass it through basic_auth and skip the encoding.

A wrong password does not look the same on every request. We measured both shapes against the local proxy.

targetwhat a wrong password does
https:// site (tunnel)The request fails. The error chain ends in tunnel error: proxy authorization required, is_connect() is true, and no status code is exposed.
http:// site (forwarding)No error. The response has status 407 and a Proxy-Authenticate: Basic header.

The number 407 never appears in the tunnel error, which trips up code that searches for it. RFC 9110 requires the proxy to send Proxy-Authenticate with every 407, so on plain HTTP targets you can read the challenge. The full fix list is in how to fix 407 Proxy Authentication Required.

Why does the proxy URL start with http:// for an https:// site?

Because the hop to the proxy and the connection to the site are two different things. For an https:// target, reqwest connects to the proxy over plain HTTP and sends a CONNECT request. RFC 9110 defines the method: the proxy establishes "a tunnel to the destination origin server" and then limits itself to "blind forwarding of data, in both directions". The TLS handshake with the site happens inside the tunnel. The proxy never decrypts anything.

The tunnel carries HTTP/2 as well. Our request to httpbin.org reported HTTP/2.0 both direct and through the local proxy, and HTTP/1.1 once we set .http1_only().

We also tried the wrong scheme. Proxy::all("https://127.0.0.1:8899") against a proxy that speaks plain HTTP waited ten seconds for a TLS handshake that never came.

error sending request for url (https://httpbin.org/ip)
  <- client error (Connect)
  <- tunnel error: failed to create underlying connection
  <- tls handshake eof

An https:// proxy URL is right only when the proxy itself terminates TLS. Those proxies exist, and reqwest 0.12.25 (December 2025) fixed the Proxy-Authorization header for them on HTTP targets. Public lists that say "HTTPS proxy" almost always mean an http:// proxy that supports CONNECT, which is the confusion behind a November 2025 thread on users.rust-lang.org.

One more finding matters here. A scheme that reqwest does not know is not an error. We built proxies with ftp://, socks://, sock5:// and https5:// and pointed them at our own SOCKS server. Every Proxy::all call succeeded, every request answered 200, and the server log showed no connection at all. The requests went direct. The parser inside hyper-util drops a scheme it does not know, so a typo in the scheme silently removes the proxy. reqwest knows http, https, socks4, socks4a, socks5 and socks5h, and it treats a URL without a scheme as http://.

How do I use a SOCKS5 proxy?

SOCKS support is compiled out unless you enable the socks feature. Without it, Proxy::all("socks5://host:1080") builds without complaint, and every request then fails.

error sending request for url (https://httpbin.org/ip)
  <- client error (Connect)
  <- tunnel error: failed to create underlying connection
  <- unsupported scheme socks5

With the feature enabled, a SOCKS URL works anywhere a proxy URL is accepted, and a missing port defaults to 1080, the conventional SOCKS port of RFC 1928.

let client = Client::builder()
    .proxy(Proxy::all("socks5h://user:pass@host:1080")?)
    .build()?;

reqwest accepts socks4, socks4a, socks5 and socks5h. The letter decides who resolves the hostname. We ran both forms against our own SOCKS5 server, which logs the address type of every request.

schemewho resolves the namewhat the proxy received in our log
socks5://your machine, before the handshake.Address type 0x01, an IPv4 address of httpbin.org.
socks5h://the proxy.Address type 0x03, the name httpbin.org.

Use socks5h when the DNS lookup must not leave through your own connection, or when the name only resolves on the network of the proxy. For the protocol itself, see what is a SOCKS5 proxy.

Credentials go into the URL, and the SOCKS handshake carries them through the username and password method of RFC 1929. .basic_auth() works too on 0.13.4, because reqwest writes it into the same URL. Our server logged user/pass OK for both forms. Two errors come from the handshake, and one of them has a misleading text.

what you diderror chain ends in
Wrong password.error connecting to socks proxy <- SOCKS error: credentials not accepted.
No credentials, proxy requires them.SOCKS error: server does not support user/pass authentication.
socks5:// URL pointed at an HTTP proxy port.SOCKS error: io error during SOCKS handshake, after ten seconds.

The second line blames the server, but our log shows the client offered no authentication method, and the server answered 0xFF, no acceptable methods. Add the credentials.

A warning about public SOCKS entries, from our scan on 2 September 2026. None of 40 socks5:// entries from our list worked through reqwest, and every one failed the same way: invalid peer certificate: UnknownIssuer. The proxy accepted the handshake and then presented a certificate for httpbin.org that no trusted root signs. That is TLS interception. As socks5h:// the picture was the same, 35 of 40 with that certificate error and the rest refused or broken. Drop such an entry. Never fix that error with danger_accept_invalid_certs.

Why is reqwest using a proxy I never set?

Because system proxies are on by default. The reqwest documentation lists the variables: HTTP_PROXY or http_proxy for HTTP connections, HTTPS_PROXY or https_proxy for HTTPS connections, and ALL_PROXY or all_proxy for both. The more specific variable wins. With the default system-proxy feature, reqwest also reads the Windows and macOS proxy settings. We set HTTPS_PROXY to a proxy with a wrong password and built clients in seven ways.

callresult
Client::new()proxy authorization required, so the variable was used.
Client::builder().no_proxy()Direct, 200.
Client::builder().proxy(good)200 through the explicit proxy, which wins.
Client::new() with an http:// targetDirect, 200: HTTPS_PROXY does not cover http://.
Client::new() plus NO_PROXY=httpbin.orgDirect, 200.
HTTPS_PROXY wrong and ALL_PROXY good, both setFailed on HTTPS_PROXY, the specific one wins.
https_proxy=socks5h://127.0.0.1:1085, lower case200 through SOCKS, with the socks feature on.

The NO_PROXY rules follow curl. They apply to the variable and to Proxy::no_proxy(NoProxy::from_string(...)) alike, and we confirmed the second form against the wrong-password proxy.

NO_PROXY entryeffect
example.com or .example.comBypasses example.com and every subdomain.
192.168.1.0/24Bypasses that range; single IPv4 and IPv6 addresses work too.
*Bypasses everything, the only wildcard allowed.

Adding a .proxy() to the builder disables the automatic system proxy, and .no_proxy() clears every proxy. The Windows path had its own bugs: version 0.12.16 applied only the HTTP setting from the Windows dialog (issue 2715, June 2025), and NO_PROXY reports kept coming (issue 2815, September 2025). When a program behaves differently in CI or on a colleague's laptop, print the variables before you debug the code.

Can I set the proxy per request instead of per client?

No. RequestBuilder has a timeout method and no proxy method, and the request for one (issue 804) has been open since February 2020. The proxy belongs to the Client, and the Client keeps a connection pool.

That pool decides how Proxy::custom behaves, and this is where the old advice on the web goes wrong. The closure is consulted when a new connection is opened, not for every request. We counted its calls.

setuprequestsclosure calls
Default pool, one host51
.pool_max_idle_per_host(0), one host55
Default pool, five different hosts55

Then we let a closure alternate two local SOCKS proxies and counted the tunnels each proxy opened.

rotation method, six requests to one hosttunnels on proxy Atunnels on proxy B
Proxy::custom closure, default pool10
Proxy::custom closure, pool_max_idle_per_host(0)33
One Client per proxy, alternated by hand11
A new Client for every request33

With the default pool, all six requests went through proxy A. The closure ran once, the connection was reused, and proxy B never saw a byte. A GitHub report from January 2024 describes the same surprise (issue 2098): only requests that opened new connections reached the proxy. One Client per proxy is the clean pattern. Each client keeps its own pooled connection, and you pick the client per request.

use reqwest::{Client, Proxy};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let list = Client::builder()
        .user_agent("my-scraper/1.0")
        .build()?
        .get("https://hproxy.com/api/proxy-list?format=txt&protocol=http&recent=true&limit=20")
        .send()
        .await?
        .text()
        .await?;

    // one Client per proxy: each keeps its own pooled connection to its proxy
    let mut clients: Vec<Client> = Vec::new();
    for entry in list.lines().map(str::trim).filter(|l| !l.is_empty()) {
        let client = Client::builder()
            .proxy(Proxy::all(format!("http://{entry}"))?)
            .timeout(Duration::from_secs(10))
            .build()?;
        clients.push(client);
    }

    let mut next = 0;
    for _ in 0..3 {
        loop {
            if clients.is_empty() {
                return Err("no working proxy left".into());
            }
            let i = next % clients.len();
            next += 1;
            match clients[i].get("https://httpbin.org/ip").send().await {
                Ok(r) => {
                    println!("{} via proxy {i}: {}", r.status(), r.text().await?.trim());
                    break;
                }
                Err(e) => {
                    eprintln!("dropping proxy {i}: {e}");
                    clients.remove(i);
                }
            }
        }
    }
    Ok(())
}

The list comes from our free proxy API, which returns fresh entries as plain text without a key. Expect most entries to fail the first request. Our run at 14:34 UTC dropped nine of the first ten and then answered through three different exits.

dropping proxy 0: error sending request for url (https://httpbin.org/ip)
200 OK via proxy 1: { "origin": "95.211.174.135" }
dropping proxy 2: error sending request for url (https://httpbin.org/ip)
...
200 OK via proxy 10: { "origin": "116.202.172.187" }
200 OK via proxy 0: { "origin": "103.237.102.191" }

If you must keep a single Client, set .pool_max_idle_per_host(0) so every request opens a fresh connection through the closure. That costs a new tunnel per request, which the last table row shows.

Should I use the blocking client or ureq?

reqwest has a blocking client behind the blocking feature. The proxy API is the same, and only the .await disappears.

let client = reqwest::blocking::Client::builder()
    .proxy(reqwest::Proxy::all("http://user:pass@host:port")?)
    .timeout(std::time::Duration::from_secs(20))
    .build()?;
let r = client.get("https://httpbin.org/ip").send()?;
println!("{} {}", r.status(), r.text()?);

The blocking docs set one rule: the blocking client "must not be executed within an async runtime, or it will panic when attempting to block". Our one request through a public entry answered 200 over HTTP/2 in 1.78 seconds, including the client build.

async Clientreqwest::blocking
Runtimetokio, #[tokio::main].None; it runs its own thread internally.
Proxy APIProxy::all, basic_auth, no_proxy, custom.Identical.
Ten requests through one public proxy0.80 s all at once on one Client, 1.61 s in sequence.In sequence only.
Inside an async functionThe normal choice.Panics; use spawn_blocking or the async client.

ureq is the other common choice for synchronous code. It is a separate crate with no async runtime at all. We ran the same proxies through ureq 3.4.0.

use ureq::{Agent, Proxy};

let proxy = Proxy::new("http://user:pass@host:8080")?;
let agent = Agent::new_with_config(Agent::config_builder().proxy(Some(proxy)).build());
let mut r = agent.get("https://httpbin.org/ip").call()?;
println!("{} {}", r.status(), r.body_mut().read_to_string()?);
reqwest 0.13.4ureq 3.4.0
ReleasedMay 2026, Rust 1.85.August 2026, Rust 1.85.
Proxy is set onthe Client.the Agent config.
SOCKSsocks feature; socks5 resolves locally, socks5h on the proxy.socks-proxy feature; same split since 3.2.0.
Wrong proxy password, https targettunnel error: proxy authorization required.CONNECT proxy failed: proxy server responded 407/407.
Wrong SOCKS passwordSOCKS error: credentials not accepted.io: password authentication failed.
Dead proxy, closed porttcp connect error plus the OS text, in the system language.io: Connection refused, always in English.
Unreachable proxy, no timeout setWaits for the OS, 21 seconds on Windows.Waits for the global timeout you configure.
Environment variablesHTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY by default; .no_proxy() disables.ALL_PROXY, HTTPS_PROXY, HTTP_PROXY, NO_PROXY by default; proxy(None) disables.
Proxy per requestNo.No.

Both crates read the environment on their own, both put the proxy on the long-lived object, and both refuse per-request proxies. Pick by the rest of your program, not by the proxy support.

What do the errors mean, and how do I fix them?

Every line below comes from our runs on reqwest 0.13.4 on 2 September 2026. The table shows the innermost text of the error chain. The operating system texts follow the system language, and ours is German.

innermost textwhyfix
tunnel error: proxy authorization requiredwrong or missing credentials, https targetfix the credentials, or encode the password.
status 407, no errorsame, on an http targetcheck r.status().
builder error <- invalid port number/ or # in a URL passwordpercent-encode it, or use basic_auth.
tls handshake eofhttps:// scheme for a plain proxyuse http://.
unsupported scheme socks5no socks featureadd the feature to Cargo.toml.
request goes direct, no errora scheme reqwest does not know, such as socks://use socks5h:// or http://.
SOCKS error: credentials not acceptedwrong SOCKS passwordfix the URL userinfo.
SOCKS error: server does not support user/pass authenticationno credentials sent, proxy requires themadd the credentials.
SOCKS error: io error during SOCKS handshakesocks5:// against an HTTP proxy portuse http://, or the right port.
operation timed outthe total timeout expired, or http:// against a SOCKS portnext proxy, or the right scheme.
tcp connect error <- deadline has elapsedconnect_timeout expirednext proxy.
tcp connect error <- ... (os error 10061)nothing listens on that portthe entry is dead, next proxy.
tcp connect error <- ... (os error 10060)no timeout set, the OS gave up after 21 secondsset connect_timeout.
tunnel error: unsuccessfulthe proxy refused the CONNECT, 400 or 503 behind itthe entry does not tunnel HTTPS, next proxy.
invalid peer certificate: UnknownIssuer through a proxythe proxy intercepts TLSdrop the entry, never danger_accept_invalid_certs.

Timeouts

reqwest sets no timeout by default. An unreachable proxy therefore waits for the operating system, 21 seconds on our Windows machine, and a proxy that accepts the connection and then stays silent waits forever. Set two budgets.

let client = Client::builder()
    .proxy(Proxy::all("http://user:pass@host:8080")?)
    .connect_timeout(Duration::from_secs(5))   // the hop to the proxy
    .timeout(Duration::from_secs(30))          // the whole request
    .build()?;

Against an unreachable address, connect_timeout(3 s) failed at 3.00 seconds with deadline has elapsed. A total timeout(5 s) failed at 5.01 seconds with operation timed out. A RequestBuilder::timeout overrides the total budget for one request.

Reading the error

The Display text of a reqwest error is only the kind and the URL, for example error sending request for url (https://httpbin.org/ip). The cause sits in the source chain. Walk it, or print the error with {:?}.

use std::error::Error as StdError;

fn chain(e: &dyn StdError) -> String {
    let mut parts = vec![e.to_string()];
    let mut cur = e.source();
    while let Some(s) = cur {
        parts.push(s.to_string());
        cur = s.source();
    }
    parts.join(" <- ")
}

For the wrong-password case, {e:?} prints source: hyper_util::client::legacy::Error(Connect, ProxyAuthRequired), which is the only place the cause is named in one word.

Catching them

is_connect() was true for every proxy failure in our matrix except the total timeout, and is_timeout() was true for both timeouts. Drop a proxy from a pool on is_connect(), retry on is_timeout(), and treat is_builder() as a bug in the URL you built.

How do I check that the proxy really carries my traffic?

Never assume. Compare what the target sees with and without the proxy before you trust an exit with real traffic.

use reqwest::{Client, Proxy};
use serde_json::Value;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let proxy = "http://user:pass@host:port";
    let direct = Client::builder().no_proxy().timeout(Duration::from_secs(10)).build()?;
    let proxied = Client::builder()
        .proxy(Proxy::all(proxy)?)
        .timeout(Duration::from_secs(15))
        .build()?;

    let real: Value = direct.get("https://httpbin.org/ip").send().await?.json().await?;
    let via: Value = proxied.get("https://httpbin.org/ip").send().await?.json().await?;
    println!("direct: {}", real["origin"]);
    println!("proxy:  {}", via["origin"]);
    assert_ne!(real["origin"], via["origin"], "the proxy is not changing your IP");

    let headers: Value = proxied.get("https://httpbin.org/headers").send().await?.json().await?;
    let leak: Vec<&String> = headers["headers"]
        .as_object()
        .unwrap()
        .keys()
        .filter(|k| matches!(k.to_ascii_lowercase().as_str(), "x-forwarded-for" | "via" | "forwarded"))
        .collect();
    println!("proxy headers seen by the target: {leak:?}");
    Ok(())
}

The no_proxy() on the first client matters, because an ALL_PROXY variable would otherwise route the "direct" request too. If the two origins match, the request never went through the proxy. The second call catches a transparent proxy that forwards your real address in X-Forwarded-For while the origin looks changed. Through the entry from the screenshot the output was the proxy address and an empty header list.

We ran the same check against public entries from our own list, pulled with the API at that minute, 20 at a time with a 10 second timeout.

scan on 2 September 2026entriesalivemost common failurefastest alive
http://, 14:18 UTC4015tunnel error: unsuccessful (18)0.47 s.
http://, 14:31 UTC4014tunnel error: unsuccessful (23)0.46 s.
socks5://, 14:19 UTC400invalid peer certificate: UnknownIssuer (40)none.
socks5h://, 14:19 UTC400invalid peer certificate: UnknownIssuer (35)none.

Of the 15 alive entries, 13 reported their own address as the origin and 2 exited through a different address. None forwarded our address in a header. Between the two scans the list rotated 12 of its 40 entries, and every entry that stayed listed stayed alive. That is the free path: a third of the entries work at any minute, and the list changes under you. Our proxy checker runs the same battery in one paste: exit IP, anonymity grade, geolocation and latency. When a project needs exits that stay alive, our residential pool is the next step. It gives one endpoint and a fresh IP per request, and the code above does not change.

Where to go from here

The proxy side of reqwest is small once three facts are clear: the proxy lives on the Client, the environment fills it in when you do not, and the rule is evaluated per connection. The work that remains is proving each exit and keeping a pool alive. The free proxy list re-checks its entries every few minutes, and the free API hands them to your program.

The Go guide is the closest sibling, another compiled language where the proxy rides on the client. The cURL guide is the fastest shell-side check of a proxy before you wire it into Rust. Proxies for web scraping covers choosing the proxy type and the request hygiene no client library adds. For production, our paid pools give you IPs nobody else is burning, and the API documentation covers key creation, ordering and plan generation.

Sources

  • reqwest 0.13.4 crate documentation, Proxies section and feature list (docs.rs, May 2026). Environment variables, precedence, no_proxy, the socks and system-proxy features.
  • reqwest 0.13.4, Proxy, ClientBuilder, NoProxy, Error and blocking documentation (docs.rs, May 2026). Constructors, rule order, basic_auth, timeouts, the bypass rules, the runtime rule.
  • reqwest source at tag v0.13.4, src/connect.rs and src/proxy.rs (GitHub, May 2026). SOCKS scheme list, local vs proxy name resolution, basic_auth written into the URL, default port 1080.
  • reqwest CHANGELOG (GitHub, entries 0.9.6 to 0.13.4). Dates of basic_auth, the socks feature, no_proxy, ALL_PROXY precedence, SOCKS4, port 1080, the HTTPS proxy fix, the rustls default.
  • crates.io API, versions of reqwest, ureq, tokio and hyper-util (read 2 September 2026). Release dates and Rust floors.
  • hyper-util 0.1.20, client::proxy::matcher documentation and source (docs.rs, February 2026). The curl-style variable rules and the handling of unknown schemes.
  • ureq 3.4.0 documentation, Proxying section, Proxy and CHANGELOG (docs.rs, August 2026). Feature name, environment order, socks5 vs socks5h since 3.2.0, error texts.
  • RFC 9110, HTTP Semantics (IETF, June 2022). Section 9.3.6 CONNECT, 15.5.8 407, 11.7.1 Proxy-Authenticate.
  • RFC 1928, SOCKS Protocol Version 5, and RFC 1929, Username/Password Authentication for SOCKS V5 (IETF, March 1996). Port 1080, methods, address types, the status byte.
  • RFC 7617, The Basic HTTP Authentication Scheme (IETF, September 2015).
  • GitHub seanmonstar/reqwest issues 804, 2098, 598, 2715, 2815, 2870 and 2748, and users.rust-lang.org thread 135156 (2019 to 2025). The questions this page answers.
  • Our own measurements on 2 September 2026. reqwest 0.13.4 with and without the socks feature and ureq 3.4.0 on rustc 1.94.0, against proxy.py 2.4.10 with Basic auth and our own SOCKS5 server; 40 plus 40 public http entries and 40 public socks5 entries from hproxy.com/free-proxy-list through reqwest.

Frequently asked questions

How do I set a proxy for one request instead of the whole Client?
You cannot. reqwest configures proxies on the Client, and RequestBuilder has no proxy method; the request for one (issue 804) has been open since 2020. Keep one Client per proxy and pick the Client per request. A Proxy::custom closure only runs when a new connection opens, so with connection pooling it does not rotate per request.
How do I use a SOCKS5 proxy with reqwest?
Enable the socks feature, reqwest = { version = "0.13", features = ["socks"] }, and pass a socks5h:// URL to Proxy::all, with the username and password in the URL if the proxy needs them. Without the feature the Proxy still builds, and every request fails with 'unsupported scheme socks5'.
What is the difference between socks5 and socks5h in reqwest?
With socks5:// reqwest resolves the hostname on your machine and sends the proxy an IPv4 address. With socks5h:// it sends the name and the proxy resolves it. We watched both on our own SOCKS5 server: address type 0x01 for socks5, 0x03 for socks5h. Use socks5h when DNS lookups must not leave through your own connection.
How do I add a username and password to a reqwest proxy?
Chain .basic_auth("user", "pass") onto the Proxy, or write them into the URL as http://user:pass@host:port. For HTTP proxies this sends a Proxy-Authorization Basic header. The same basic_auth call also works for SOCKS5 proxies on 0.13.4, because reqwest writes the credentials into the proxy URL.
Why does my reqwest error only say 'error sending request'?
Because the Display text of a reqwest error carries only the kind and the URL. The cause sits in the source chain. Print the error with {:?} or walk std::error::Error::source() to see 'proxy authorization required', 'tcp connect error' or 'unsupported scheme socks5'.
Why is reqwest using a proxy I never configured?
System proxies are on by default. reqwest reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY from the environment, plus the Windows and macOS proxy settings. An explicit .proxy() wins over them, and .no_proxy() on the builder ignores them.
Does a wrong proxy password give a 407 in reqwest?
Only for http:// targets, where the 407 arrives as a normal response with a Proxy-Authenticate header. For https:// targets the CONNECT tunnel fails first, and the error chain ends in 'tunnel error: proxy authorization required' with no status code at all.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires. $0.44/GB is the 2,000 GB+ rate; a single gigabyte is $0.50/GB, with no minimum order.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed