To use a proxy with PHP cURL, set CURLOPT_PROXY on the request handle, supply proxy credentials through separate options, keep certificate verification enabled, and check both the transfer result and HTTP status. Start with one approved destination and a direct baseline before adding concurrency or retries.
Reviewed and locally tested: October 12, 2026. This guide uses native PHP cURL, without Composer or Guzzle. The executed runtime was Windows PHP 8.4.22, libcurl 8.20.0, and OpenSSL 3.0.20. These results do not qualify production PHP 7 or every PHP build.
Check the PHP runtime making the request
php --ri curl
php -r '$v = curl_version(); echo PHP_VERSION, " / ", $v["version"], PHP_EOL;'
Check the CLI, FPM worker, or web server that actually runs your application. Different PHP installations can load different extensions and trust bundles. Our Windows CLI had cURL disabled initially; the lab loaded its existing extension with process-only -n -d extension_dir=PATH -d extension=curl, leaving php.ini unchanged. Configure an application runtime deliberately rather than copying that diagnostic invocation blindly.
The extension talks to libcurl, whose version and TLS backend matter independently of PHP. See the PHP cURL options reference. Use the Guzzle guide when your application already uses its higher-level client; this article covers native handles.
Use the tested request helper
The developer examples ZIP includes request.php; that file contains mexelaCurlRequest() and a standalone CLI entry point. Save it beside your diagnostic script. Supply an approved CHECK_URL and a credential-free PROXY_ENDPOINT, such as http://proxy.example:8080, through the process environment. Keep the username and password in separate secret variables.
<?php
require __DIR__ . '/request.php';
$result = mexelaCurlRequest([
'url' => getenv('CHECK_URL'),
'proxy' => [
'endpoint' => getenv('PROXY_ENDPOINT'),
'username' => getenv('PROXY_USER'),
'password' => getenv('PROXY_PASSWORD'),
],
'connect_timeout_ms' => 2000,
'total_timeout_ms' => 5000,
]);
// Inspect the body in application memory; log only selected metadata.
unset($result['body']);
echo json_encode($result), PHP_EOL;
The helper limits destination protocols to HTTP/HTTPS and does not follow redirects. It applies these native settings to one handle:
CURLOPT_PROXY => $endpoint,
CURLOPT_PROXYUSERNAME => $username,
CURLOPT_PROXYPASSWORD => $password,
CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
CURLOPT_NOPROXY => '',
CURLOPT_CONNECTTIMEOUT_MS => 2000,
CURLOPT_TIMEOUT_MS => 5000,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
Separate credential options avoid URL-encoding mistakes and accidental credential-bearing URLs. An HTTP connection to the proxy does not encrypt Basic authentication; use the approved transport and network path for your endpoint. The official proxy username and password options describe these fields.
Make the route explicit
Call the same helper with only ['url' => $approvedUrl] for a direct baseline. It sets CURLOPT_PROXY to an empty string, disabling inherited proxies. For a selected proxy it also sets CURLOPT_NOPROXY to an empty string, overriding inherited bypass lists. That choice is confined to this handle; review it against your application’s intentional internal-host exceptions.
A destination-side observation establishes which route handled the request. A 200 response alone does not. Our destination correlated actual upstream sockets with the direct, HTTP-proxy, or SOCKS route. Read libcurl proxy selection and the environment-variable guide before mixing explicit routing with process-wide variables.
HTTPS tunnels, CA files, and SOCKS5
An HTTPS destination can use an http:// proxy through CONNECT. The destination scheme and proxy transport are separate choices; do not change the endpoint scheme merely because the destination uses HTTPS. Keep peer verification enabled and hostname verification at 2.
When an approved private CA is required, pass 'ca_file' => '/approved/path/ca.pem' to the helper. It sets destination CURLOPT_CAINFO for that request. Obtain the CA through your trusted administration process; downloading whatever certificate appears in a failed connection does not establish trust. See libcurl certificate verification. An HTTPS proxy’s own certificate trust is a separate configuration not covered by this lab.
For a supported SOCKS endpoint, use socks5h://proxy.example:1080 with the same separate credentials. libcurl documents socks5h as proxy-side destination hostname resolution. Our authenticated SOCKS test used an IP literal, so it establishes tunneling and TLS validation, not DNS privacy or hostname-resolution behavior.
Measured local observations
| Native PHP cURL check | Observed result |
|---|---|
| Direct and authenticated HTTP requests | 200; destination distinguished direct and proxy routes |
| Wrong/missing HTTP proxy credentials | 407; no destination request; transfer itself completed |
| Wrong CONNECT credentials | Tunnel 407; destination status 0; cURL errno 7 on this build |
| Untrusted HTTPS certificate | CONNECT 200, then cURL errno 60 |
| Approved request CA and authenticated SOCKS5 | 200 with strict TLS; destination identified the selected route |
| Redirect and stalled response | 302 returned without following; 350 ms timeout measured 351.9 ms, errno 28 |
All 15 lab checks passed against owned loopback fixtures. The destination received no Proxy-Authorization header. Random fixture credentials stayed in child input/environment; the disposable private key stayed in memory. No commercial endpoint, public exit, country, anonymity, speed, uptime, or PHP 7 compatibility was tested.
Diagnose the failing layer
Compare curl_exec() strictly with false, then inspect curl_errno(), destination status, and CURLINFO_HTTP_CONNECTCODE. The PHP execution reference explains why an HTTP error can still be a successful transfer; the CONNECT status reference separates the tunnel response.
For 407, check proxy credentials or allowlisting; a destination 401/403 belongs to a different layer. For errno 60, investigate the approved trust chain and hostname. For errno 28, distinguish connection setup from a stalled transfer before retrying. The total timeout includes connection time, but resolver/backend behavior can affect precise timing. Redirects remain disabled to keep the diagnostic on its selected endpoint.
Frequently asked questions
Does configuring PHP cURL route every PHP request?
No. These options apply to the configured handle. Other handles, Guzzle clients, browser traffic, and server processes require their own route checks.
Can I print curl_error() or enable verbose logging?
Use a controlled private diagnostic when necessary. Raw errors, URLs, headers, response bodies, and environment dumps may expose sensitive information; the starter emits only selected numeric metadata.
Continue with Developer Guides, command-line cURL, our testing methodology, and the proxy verification checklist.
Versioned PHP cURL source files
Use the PHP cURL folder in the public source repository for the example, setup instructions, exact dependency versions and a sanitized test summary. The repository is a dated snapshot of owned examples; its loopback checks do not certify a commercial endpoint or every application configuration.
