{"id":1098,"date":"2026-10-12T20:46:46","date_gmt":"2026-10-12T17:46:46","guid":{"rendered":"https:\/\/mexela.com\/blog\/php-curl-proxy\/"},"modified":"2026-10-13T20:19:37","modified_gmt":"2026-10-13T17:19:37","slug":"php-curl-proxy","status":"publish","type":"post","link":"https:\/\/mexela.com\/blog\/php-curl-proxy\/","title":{"rendered":"PHP cURL Proxy Setup: Authentication, HTTPS CONNECT and Safe Diagnostics"},"content":{"rendered":"<p class=\"mexela-answer\">To use a proxy with PHP cURL, set <code>CURLOPT_PROXY<\/code> 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.<\/p>\n<p><strong>Reviewed and locally tested: October 12, 2026.<\/strong> 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.<\/p>\n<h2 id=\"check-runtime\">Check the PHP runtime making the request<\/h2>\n<pre><code class=\"language-bash\">php --ri curl\nphp -r '$v = curl_version(); echo PHP_VERSION, \" \/ \", $v[\"version\"], PHP_EOL;'<\/code><\/pre>\n<p>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 <code>-n -d extension_dir=PATH -d extension=curl<\/code>, leaving <code>php.ini<\/code> unchanged. Configure an application runtime deliberately rather than copying that diagnostic invocation blindly.<\/p>\n<p>The extension talks to libcurl, whose version and TLS backend matter independently of PHP. See the <a href=\"https:\/\/www.php.net\/manual\/en\/curl.constants.php\">PHP cURL options reference<\/a>. Use <a href=\"\/blog\/php-guzzle-proxy\/\">the Guzzle guide<\/a> when your application already uses its higher-level client; this article covers native handles.<\/p>\n<h2 id=\"request\">Use the tested request helper<\/h2>\n<p>The <a href=\"\/blog\/wp-content\/uploads\/2026\/10\/mexela-developer-examples-20261012-v1.zip\">developer examples ZIP<\/a> includes <code>request.php<\/code>; that file contains <code>mexelaCurlRequest()<\/code> and a standalone CLI entry point. Save it beside your diagnostic script. Supply an approved <code>CHECK_URL<\/code> and a credential-free <code>PROXY_ENDPOINT<\/code>, such as <code>http:\/\/proxy.example:8080<\/code>, through the process environment. Keep the username and password in separate secret variables.<\/p>\n<pre><code class=\"language-php\">&lt;?php\nrequire __DIR__ . '\/request.php';\n\n$result = mexelaCurlRequest([\n    'url' =&gt; getenv('CHECK_URL'),\n    'proxy' =&gt; [\n        'endpoint' =&gt; getenv('PROXY_ENDPOINT'),\n        'username' =&gt; getenv('PROXY_USER'),\n        'password' =&gt; getenv('PROXY_PASSWORD'),\n    ],\n    'connect_timeout_ms' =&gt; 2000,\n    'total_timeout_ms' =&gt; 5000,\n]);\n\n\/\/ Inspect the body in application memory; log only selected metadata.\nunset($result['body']);\necho json_encode($result), PHP_EOL;<\/code><\/pre>\n<p>The helper limits destination protocols to HTTP\/HTTPS and does not follow redirects. It applies these native settings to one handle:<\/p>\n<pre><code class=\"language-php\">CURLOPT_PROXY =&gt; $endpoint,\nCURLOPT_PROXYUSERNAME =&gt; $username,\nCURLOPT_PROXYPASSWORD =&gt; $password,\nCURLOPT_PROXYAUTH =&gt; CURLAUTH_BASIC,\nCURLOPT_NOPROXY =&gt; '',\nCURLOPT_CONNECTTIMEOUT_MS =&gt; 2000,\nCURLOPT_TIMEOUT_MS =&gt; 5000,\nCURLOPT_FOLLOWLOCATION =&gt; false,\nCURLOPT_SSL_VERIFYPEER =&gt; true,\nCURLOPT_SSL_VERIFYHOST =&gt; 2,<\/code><\/pre>\n<p>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 <a href=\"https:\/\/curl.se\/libcurl\/c\/CURLOPT_PROXYUSERNAME.html\">proxy username<\/a> and <a href=\"https:\/\/curl.se\/libcurl\/c\/CURLOPT_PROXYPASSWORD.html\">password options<\/a> describe these fields.<\/p>\n<h2 id=\"direct-baseline\">Make the route explicit<\/h2>\n<p>Call the same helper with only <code>['url' =&gt; $approvedUrl]<\/code> for a direct baseline. It sets <code>CURLOPT_PROXY<\/code> to an empty string, disabling inherited proxies. For a selected proxy it also sets <code>CURLOPT_NOPROXY<\/code> to an empty string, overriding inherited bypass lists. That choice is confined to this handle; review it against your application&#8217;s intentional internal-host exceptions.<\/p>\n<p>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 <a href=\"https:\/\/curl.se\/libcurl\/c\/CURLOPT_PROXY.html\">libcurl proxy selection<\/a> and <a href=\"\/blog\/http-proxy-https-proxy-no-proxy\/\">the environment-variable guide<\/a> before mixing explicit routing with process-wide variables.<\/p>\n<h2 id=\"https-socks\">HTTPS tunnels, CA files, and SOCKS5<\/h2>\n<p>An HTTPS destination can use an <code>http:\/\/<\/code> 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 <code>2<\/code>.<\/p>\n<p>When an approved private CA is required, pass <code>'ca_file' =&gt; '\/approved\/path\/ca.pem'<\/code> to the helper. It sets destination <code>CURLOPT_CAINFO<\/code> for that request. Obtain the CA through your trusted administration process; downloading whatever certificate appears in a failed connection does not establish trust. See <a href=\"https:\/\/curl.se\/libcurl\/c\/CURLOPT_SSL_VERIFYPEER.html\">libcurl certificate verification<\/a>. An HTTPS proxy&#8217;s own certificate trust is a separate configuration not covered by this lab.<\/p>\n<p>For a supported SOCKS endpoint, use <code>socks5h:\/\/proxy.example:1080<\/code> with the same separate credentials. libcurl documents <code>socks5h<\/code> 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.<\/p>\n<h2 id=\"measured\">Measured local observations<\/h2>\n<table>\n<thead>\n<tr>\n<th>Native PHP cURL check<\/th>\n<th>Observed result<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Direct and authenticated HTTP requests<\/td>\n<td>200; destination distinguished direct and proxy routes<\/td>\n<\/tr>\n<tr>\n<td>Wrong\/missing HTTP proxy credentials<\/td>\n<td>407; no destination request; transfer itself completed<\/td>\n<\/tr>\n<tr>\n<td>Wrong CONNECT credentials<\/td>\n<td>Tunnel 407; destination status 0; cURL errno 7 on this build<\/td>\n<\/tr>\n<tr>\n<td>Untrusted HTTPS certificate<\/td>\n<td>CONNECT 200, then cURL errno 60<\/td>\n<\/tr>\n<tr>\n<td>Approved request CA and authenticated SOCKS5<\/td>\n<td>200 with strict TLS; destination identified the selected route<\/td>\n<\/tr>\n<tr>\n<td>Redirect and stalled response<\/td>\n<td>302 returned without following; 350 ms timeout measured 351.9 ms, errno 28<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>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.<\/p>\n<h2 id=\"diagnostics\">Diagnose the failing layer<\/h2>\n<p>Compare <code>curl_exec()<\/code> strictly with <code>false<\/code>, then inspect <code>curl_errno()<\/code>, destination status, and <code>CURLINFO_HTTP_CONNECTCODE<\/code>. The <a href=\"https:\/\/www.php.net\/manual\/en\/function.curl-exec.php\">PHP execution reference<\/a> explains why an HTTP error can still be a successful transfer; the <a href=\"https:\/\/curl.se\/libcurl\/c\/CURLINFO_HTTP_CONNECTCODE.html\">CONNECT status reference<\/a> separates the tunnel response.<\/p>\n<p>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 <a href=\"https:\/\/curl.se\/libcurl\/c\/CURLOPT_TIMEOUT_MS.html\">total timeout<\/a> includes connection time, but resolver\/backend behavior can affect precise timing. Redirects remain disabled to keep the diagnostic on its selected endpoint.<\/p>\n<h2 id=\"faq\">Frequently asked questions<\/h2>\n<h3>Does configuring PHP cURL route every PHP request?<\/h3>\n<p>No. These options apply to the configured handle. Other handles, Guzzle clients, browser traffic, and server processes require their own route checks.<\/p>\n<h3>Can I print curl_error() or enable verbose logging?<\/h3>\n<p>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.<\/p>\n<p>Continue with <a href=\"\/blog\/proxy-setup-developer-guides\/\">Developer Guides<\/a>, <a href=\"\/blog\/curl-proxy\/\">command-line cURL<\/a>, <a href=\"\/blog\/proxy-testing-methodology\/\">our testing methodology<\/a>, and <a href=\"\/blog\/test-if-your-proxy-is-working\/\">the proxy verification checklist<\/a>.<\/p>\n<h2 id=\"versioned-source-repository\">Versioned PHP cURL source files<\/h2>\n<p>Use the <a href=\"\/blog\/proxy-code-examples-repository\/#php\">PHP cURL folder in the public source repository<\/a> 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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Configure native PHP cURL with explicit proxy routing, separate credentials, strict TLS, finite timeouts and measured HTTP\/CONNECT\/SOCKS5 checks.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[189],"tags":[],"_links":{"self":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/1098"}],"collection":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/comments?post=1098"}],"version-history":[{"count":0,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/1098\/revisions"}],"wp:attachment":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/media?parent=1098"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/categories?post=1098"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/tags?post=1098"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}