Short answers
How do I set an authenticated proxy in PHP cURL?
Set CURLOPT_PROXY to the HTTP gateway, select CURLPROXY_HTTP, and put the encoded username and password in CURLOPT_PROXYUSERPWD. For an HTTPS destination, cURL establishes a CONNECT tunnel before the destination TLS connection.
Why does PHP cURL return false?
A strict false result means the transfer failed. Read curl_errno and the CONNECT response code before releasing the handle. An HTTP 401 or 403 from the destination is a separate result and must be checked through CURLINFO_HTTP_CODE.
Can NO_PROXY make this request go directly?
Yes, host exclusions can change routing. This check deliberately sets CURLOPT_NOPROXY to an empty string so the configured proxy is used even when the shell contains NO_PROXY. It stops if that proxy is unavailable.
Check the PHP runtime and connection fields
Use a supported PHP release with the cURL extension; this example’s syntax requires PHP 8.1 or later. The command-line PHP installation may differ from PHP-FPM or the one serving your website. Check the same runtime that will execute your job.
php --version
php -r 'echo extension_loaded("curl") ? curl_version()["version"] . PHP_EOL : "curl extension is missing\n";'In the connection builder, select HTTP and copy the gateway, full generated username and proxy password. The username includes the chosen pool, country and rotation settings. A website sign-in password or API key is not the proxy password.
This is a PHP CLI recipe, not a web form that accepts arbitrary destination URLs. Begin with the echo check below, then adapt it to a destination you are authorised to access. For the equivalent terminal command, use the curl connection guide.
Make one bounded HTTPS request
Save this file as proxy-check.php. Its default gateway and echo destination are filled from this site’s configuration. PROXY_SERVER can override the gateway with an HTTP scheme, host and port, without credentials; PROXY_CHECK_URL can select another trusted HTTPS echo endpoint with an ip field.
<?php
declare(strict_types=1);
function fail(string $message): never {
fwrite(STDERR, $message . PHP_EOL);
exit(1);
}
if (PHP_SAPI !== 'cli' || !extension_loaded('curl')) {
fail('Use PHP CLI with the curl extension enabled.');
}
$userLine = fgets(STDIN);
$passwordLine = fgets(STDIN);
$user = $userLine === false ? '' : rtrim($userLine, "\r\n");
$password = $passwordLine === false ? '' : rtrim($passwordLine, "\r\n");
if ($user === '' || $password === '') fail('Missing proxy credentials.');
$proxy = getenv('PROXY_SERVER') ?: 'http://gw.portproof.org:7000';
$url = getenv('PROXY_CHECK_URL') ?: 'https://api.portproof.org/v1/echo-ip';
$proxyParts = parse_url($proxy);
if (!is_array($proxyParts) || ($proxyParts['scheme'] ?? '') !== 'http'
|| empty($proxyParts['host']) || !isset($proxyParts['port'])
|| isset($proxyParts['user']) || isset($proxyParts['pass'])
|| isset($proxyParts['query']) || isset($proxyParts['fragment'])
|| !in_array($proxyParts['path'] ?? '', ['', '/'], true)) {
fail('Use an HTTP proxy URL with host and port, without credentials or a path.');
}
$urlParts = parse_url($url);
if (!is_array($urlParts) || ($urlParts['scheme'] ?? '') !== 'https'
|| empty($urlParts['host']) || isset($urlParts['user']) || isset($urlParts['pass'])) {
fail('Use an HTTPS check destination without URL credentials.');
}
$body = '';
$tooLarge = false;
$ch = curl_init($url);
if ($ch === false) fail('Could not initialise cURL.');
$configured = curl_setopt_array($ch, [
CURLOPT_PROXY => $proxy,
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
CURLOPT_HTTPPROXYTUNNEL => true,
CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
CURLOPT_PROXYUSERPWD => rawurlencode($user) . ':' . rawurlencode($password),
CURLOPT_NOPROXY => '',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$body, &$tooLarge): int {
if (strlen($body) + strlen($chunk) > 65_536) {
$tooLarge = true;
return 0;
}
$body .= $chunk;
return strlen($chunk);
},
]);
unset($user, $password, $userLine, $passwordLine);
if (!$configured) fail('Could not configure cURL.');
$caFile = getenv('PROXY_CA_FILE');
if ($caFile && !curl_setopt($ch, CURLOPT_CAINFO, $caFile)) {
fail('Could not configure the trusted CA file.');
}
$ok = curl_exec($ch);
$error = curl_errno($ch);
$connect = (int) curl_getinfo($ch, CURLINFO_HTTP_CONNECTCODE);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
unset($ch);
if ($connect === 407) fail('Proxy authentication rejected (CONNECT 407).');
if ($tooLarge) fail('Echo response exceeded 65,536 bytes.');
if ($ok === false) fail("Transfer failed: cURL $error; CONNECT $connect; HTTP $status.");
if ($connect !== 200) fail("Unexpected CONNECT status $connect.");
if ($status !== 200) fail("Destination returned HTTP $status.");
try {
$data = json_decode($body, true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
fail('Echo response was not valid JSON.');
}
$ip = is_array($data) ? ($data['ip'] ?? null) : null;
if (!is_string($ip) || filter_var($ip, FILTER_VALIDATE_IP) === false) {
fail('Echo response did not contain a valid IP address.');
}
printf("HTTP %d; exit %s\n", $status, $ip);The buffer stops at 65,536 bytes because this is a small diagnostic response, not a download client. Returning a short byte count from the write callback aborts the transfer. The connection budget is 10 seconds and the total transfer budget is 30 seconds; these are example limits, not a network performance promise.
Enter the password without putting it in a command
Save the following beside the PHP file as run-proxy-check.sh, then run bash run-proxy-check.sh from that directory in a terminal. This wrapper requires Bash and a terminal, as on macOS or a Linux shell; it is not a PowerShell command.
#!/usr/bin/env bash
set +x
set -euo pipefail
unset proxy_user proxy_password
trap 'unset proxy_user proxy_password' EXIT
IFS= read -r -p 'Full proxy username: ' proxy_user </dev/tty
IFS= read -r -s -p 'Proxy password (hidden): ' proxy_password </dev/tty
printf '\n' >/dev/tty
printf '%s\n' "$proxy_user" "$proxy_password" | php proxy-check.phpThe hidden prompt sends the password through the PHP process’s standard input. It is not added to command arguments, exported as an environment variable or written to a credential file. The script disables shell tracing before reading credentials. Do not turn tracing back on, paste a password into the script or share a terminal recording that reveals other account details.
This line-based input preserves spaces and punctuation but does not support credentials containing a newline. In an unattended worker, use your deployment’s secret delivery mechanism rather than an interactive prompt, and keep diagnostics free of request headers and credential values.
Keep proxy authentication separate from destination authentication
CURLOPT_PROXYUSERPWD is interpreted as an encoded username, a colon separator and an encoded password. The example uses rawurlencode on each value separately so a password containing a percent sign, colon or at-sign reaches the proxy unchanged. Do not encode an already encoded credential again.
CURLOPT_PROXYAUTH selects Basic authentication for the gateway. Do not replace CURLOPT_PROXYUSERPWD with CURLOPT_USERPWD, and do not add Proxy-Authorization to the destination headers. Destination authentication has a different recipient; this echo check does not send any destination credential.
The gateway URI stays http:// while the destination is https://. CONNECT carries the destination TLS connection through the HTTP proxy. That protects the destination exchange, but it does not encrypt the Basic proxy credential on the connection to an HTTP gateway. Base64 is not encryption; understand that first connection’s trust boundary before using it on a network you do not trust.
The example is scoped to an HTTP gateway. SOCKS5 uses a different proxy type and the matching SOCKS port; DNS behaviour also depends on the selected type. See HTTP and SOCKS5 explained before changing protocols.
Read the tunnel result before the destination result
PHP documents that curl_exec can succeed even when the server returns an HTTP error status. This example leaves that behaviour intact and checks both layers explicitly. CURLINFO_HTTP_CONNECTCODE describes the proxy’s last CONNECT response; zero means that no CONNECT response code was available.
| Result | Meaning | Next step |
|---|---|---|
| CONNECT 407 | The proxy rejected gateway authentication before the HTTPS request reached the destination. | Check the full generated username and proxy password; use the 407 guide below. |
| CONNECT 200, HTTP 401 or 403 | The tunnel succeeded, then the HTTPS destination refused the request. | Check the destination’s authentication and access policy. Changing proxy credentials does not fix a destination login. |
| CONNECT 200, HTTP 429 | The destination returned a rate-limit response. | Reduce work and respect its retry policy. Do not rotate repeatedly to defeat that limit. |
| HTTP 3xx | The destination returned a redirect that this check did not follow. | Review the intended destination before adding redirect handling. |
| cURL 7 | The connection could not be established. | Confirm the gateway hostname, port and outbound access. |
| cURL 28 | A configured timeout expired. | Check which stage stalled and set a measured budget for the real job. |
| cURL 60 | The destination certificate could not be verified. | Check the hostname, machine clock, certificate chain and trusted CA configuration. |
| Response limit or invalid JSON/IP | The echo response did not match this small check’s contract. | Confirm the selected endpoint and expected response without dumping its body into shared logs. |
Use the 407 checklist for rejected credentials and the CONNECT failure guide when the gateway cannot establish a tunnel. The script reports numeric diagnostics rather than raw error strings or response bodies, which may contain information unsuitable for support logs.
Keep certificate checks and the chosen route intact
Certificate-chain verification and hostname verification remain enabled. For a controlled test using your own certificate authority, PROXY_CA_FILE may point to a trusted PEM CA bundle. Leave it unset for the runtime’s normal trust store. Only use a CA file whose origin you have verified; do not disable TLS checks to make an error disappear.
The explicit empty CURLOPT_NOPROXY overrides host exclusions for this check. If your organisation requires particular hosts to be reached directly, use an approved destination for the test rather than silently overriding that network policy in a production application.
Redirect following is disabled. A larger application needs a deliberate redirect policy, including allowed schemes and hosts, before it follows a destination supplied in a response. Never forward application credentials to a new origin without checking that policy.
What we tested and what comes next
On 7 October 2026, we ran this PHP example with PHP 8.5.11 and libcurl 8.22.0 against a local authenticated CONNECT proxy and an HTTPS fixture. Checks covered reserved password characters, NO_PROXY exclusions, proxy rejection, destination errors, redirects, certificate trust and hostname failures, the response-size limit, invalid echo data and timeouts. These checks do not measure Portproof gateway availability, a pool’s speed or an exit country.
A successful result prints your own observed exit IP. It does not establish the country or guarantee that another destination will accept the request. Make one permitted application request next, check its content, and measure traffic before adding concurrent work or retries.
Portproof’s mobile and residential pools are shared. Pool, country and rotation are selected in the username, so use the builder’s current output when the job changes. A sticky session keeps the same device; the carrier may still change the IP. Read rotation and sessions, review traffic accounting, or choose your GB.
What is not allowed
Use this setup for authorised QA, lawful monitoring and research within the destination’s terms and rate limits. See the acceptable-use policy.