PHP cURL proxy with username and password
Two options look alike and only one is for the proxy. And for http:// URLs a rejected login doesn't fail at all, it comes back as a page.
In short
CURLOPT_PROXY => "via.proxies.vision:9000" plus CURLOPT_PROXYUSERPWD => "user:pass". Then check two things: curl_exec() returning false, and a response code of 407, because for http:// URLs a refused login arrives as an ordinary response.
cURL keeps two logins apart. CURLOPT_PROXYUSERPWD is sent to the proxy, in a Proxy-Authorization header. CURLOPT_USERPWD is sent to the website, in Authorization, and only after the proxy has let you through. Put your proxy login into the second one and the proxy never sees it:
The login can also go into the proxy URL itself, as in the last row. Then special characters matter: a password with an @ in it broke the URL before anything was sent (Unsupported proxy syntax … Bad hostname). Encode both parts with rawurlencode(), or keep the login in CURLOPT_PROXYUSERPWD, which needs no encoding.
For an https:// URL, cURL first asks the proxy for a tunnel. If the proxy refuses, there is no response from the website, and curl_exec() returns false with error 56. For an http:// URL there is no tunnel: the proxy answers the request itself, and cURL hands you that answer like any other page. Same wrong password, two very different results:
A scraper that saves error pages
Code that only checks for false stores the proxy’s error text as if it were the website, for every http:// URL. Check CURLINFO_RESPONSE_CODE too. For tunnels, CURLINFO_HTTP_CONNECTCODE holds the proxy’s status even when the request failed: 407, 403 or 465 on our gateway.
Every message, reproduced
PHP 8.4.26 with libcurl 8.14.1 against via.proxies.vision, https://ipinfo.io as the target.
| What we set | errno | curl_error() |
|---|---|---|
| Wrong password, or no login | 56 | CONNECT tunnel failed, response 407 |
Username -country-usa | 56 | CONNECT tunnel failed, response 403 |
A session on the rotating port 9000 | 56 | CONNECT tunnel failed, response 403 |
| Chicago with country de | 56 | CONNECT tunnel failed, response 465 |
| Password with @, not encoded, in the URL | 5 | Unsupported proxy syntax in '…': Bad hostname |
| HTTP to the SOCKS port 11000 | 56 | Proxy CONNECT aborted |
| socks5h:// to the HTTP port 9000 | 28 | Connection timed out after 40005 milliseconds |
https:// as the proxy scheme | 35 | TLS connect error: error:0A00010B:SSL routines::wrong version number |
| socks5h://, wrong password | 97 | User was rejected by the SOCKS5 server (1 1). |
| Port 8080 | 7 | Failed to connect to via.proxies.vision port 8080 … Could not connect to server |
| A typo in the host | 5 | Could not resolve proxy: via.proxy.vision |
403: a value in the username
Targeting goes into the proxy username. Our gateway refuses values it can’t read: a three-letter country code, or a session label on port 9000 (sessions belong on 10000). A key it doesn’t know is ignored without an error.
465: no exit
The login is fine, but no residential exit matches every filter right now. Drop the narrowest one, usually the city. The cURL exit codes are the same as on the command line; here is each one.
SOCKS5
Write socks5h://via.proxies.vision:11000 as the proxy, or keep host:port and set CURLOPT_PROXYTYPE to CURLPROXY_SOCKS5_HOSTNAME. Both returned 200, as did plain CURLPROXY_SOCKS5. The _HOSTNAME form sends the website’s name to the proxy instead of looking it up on your machine, so no DNS request leaves from your own connection.
Guzzle runs on the same cURL, so the messages above appear inside its exceptions: cURL error 56: CONNECT tunnel failed, response 407. Its proxy option takes a URL with the login in it. It also takes an array keyed by URL scheme, and that is where requests quietly skip the proxy:
No error, just your IP
With only an 'http' key, an https:// request returned 200 with the same IP as a request without any proxy. Nothing failed, so nothing tells you. The key is the scheme of the website’s URL, not of the proxy, and almost every site is https:// today.
Guzzle 7 and 8 throw different classes
A refused tunnel threw RequestException in Guzzle 7.16 and NetworkException in 8.3. An http:// URL with a wrong login threw ClientException (a 407 response) in both. Catch the GuzzleException interface and the code works on either version.
A setup that works
Both scripts ran unchanged with php on PHP 8.4, the Guzzle one with Guzzle 8.3 and 7.16.
<?php
// php fetch_proxy.php (PHP with the curl extension)
$user = getenv("PROXY_USER") . "-country-de";
$pass = getenv("PROXY_PASS");
$ch = curl_init("https://ipinfo.io/json");
curl_setopt_array($ch, [
CURLOPT_PROXY => "via.proxies.vision:9000",
CURLOPT_PROXYUSERPWD => "$user:$pass", // the proxy's login; CURLOPT_USERPWD is the website's
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
if ($body === false) {
// https:// URLs: a refused tunnel ends here, e.g. "CONNECT tunnel failed, response 407"
fwrite(STDERR, "curl error " . curl_errno($ch) . ": " . curl_error($ch) . "\n");
exit(1);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status === 407) {
// http:// URLs: the proxy's 407 arrives as an ordinary response, not an error
fwrite(STDERR, "The proxy rejected the login\n");
exit(1);
}
echo $status, " ", $body, "\n";200 {
"ip": "79.226.48.89",
"city": "Köln",
…<?php
require __DIR__ . "/vendor/autoload.php"; // composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
$user = getenv("PROXY_USER") . "-country-de";
$pass = getenv("PROXY_PASS");
// One string, not an array: every URL scheme goes through the proxy.
// rawurlencode keeps characters like @ or : in a password from breaking the URL.
$proxy = sprintf("http://%s:%s@via.proxies.vision:9000", rawurlencode($user), rawurlencode($pass));
$client = new Client(["proxy" => $proxy, "timeout" => 30]);
try {
$r = $client->get("https://ipinfo.io/json");
echo $r->getStatusCode(), " ", $r->getBody(), "\n";
} catch (GuzzleException $e) {
// A refused tunnel reads "cURL error 56: CONNECT tunnel failed, response 407"
fwrite(STDERR, $e->getMessage() . "\n");
exit(1);
}Port 9000 gives a new exit per connection. For the same IP across requests, add a session to the username and use port 10000: USER-country-de-session-job1-lifetime-10.
How do I use a proxy with a username and password in PHP cURL?
Why does curl_exec return a page instead of failing when the proxy login is wrong?
How do I use a SOCKS5 proxy in PHP cURL?
Why does my Guzzle request not use the proxy?
What does 'Unsupported proxy syntax … Bad hostname' mean?
Keep reading
Rust reqwest Proxy: Authentication, SOCKS5 and 'tunnel error: unsuccessful'
Use a proxy with a login in Rust reqwest: Proxy::all and basic_auth, the socks feature, what 'tunnel error: unsuccessful' hides, and a working example.
5 min read · #authentication#errors
aiohttp Proxy Errors: ClientHttpProxyError 407, SOCKS5 and trust_env
aiohttp's ClientHttpProxyError explained, why its message leaks your proxy password, socks5:// without aiohttp-socks, and rotation.
6 min read · #python#errors
curl: (56) CONNECT tunnel failed, response 407 — and git's Proxy CONNECT aborted
What curl's 'CONNECT tunnel failed, response 407' means, every curl proxy exit code we hit, and why git fails through a proxy that works in curl.
6 min read · #errors#authentication
Residential proxies, pay per GB
HTTP and SOCKS5. Country, city and sticky sessions in the username. No subscription.