PHP API clients can keep using HTTP/1.1 even when the target endpoint supports HTTP/2. Set PHP cURL to request HTTP/2 on the transfer, then verify the negotiated protocol after the request instead of assuming the option was honored.
PHP exposes libcurl protocol selection through CURLOPT_HTTP_VERSION. For normal HTTPS API requests, CURL_HTTP_VERSION_2TLS asks libcurl to attempt HTTP/2 during TLS negotiation and fall back to HTTP/1.1 when the server, proxy, or client build cannot negotiate HTTP/2.
The PHP extension must be built against a libcurl version with HTTP/2 support. Use curl_version() before the request to confirm the client feature and curl_getinfo() with CURLINFO_HTTP_VERSION after curl_exec() to prove the transfer actually used HTTP/2.
Steps to enable HTTP/2 with PHP cURL:
- Confirm the PHP cURL extension is loaded in the runtime that will run the request.
$ php -r "var_export(extension_loaded('curl')); echo PHP_EOL;" trueInstall or enable the platform's PHP cURL extension package when this prints false. On Debian and Ubuntu systems, the package is commonly named php-curl.
- Create a request script that checks client HTTP/2 support, requests HTTP/2 over HTTPS, and prints the negotiated protocol.
- http2-request.php
<?php $url = $argv[1] ?? 'https://nghttp2.org/httpbin/get'; $version = curl_version(); $featureList = $version['feature_list'] ?? []; $http2Supported = ($featureList['HTTP2'] ?? false) || (defined('CURL_VERSION_HTTP2') && (($version['features'] & CURL_VERSION_HTTP2) !== 0)); echo 'cURL library: ' . $version['version'] . PHP_EOL; echo 'HTTP/2 feature: ' . ($http2Supported ? 'yes' : 'no') . PHP_EOL; if (!$http2Supported || !defined('CURL_HTTP_VERSION_2TLS')) { fwrite(STDERR, 'This PHP cURL build does not expose HTTP/2 support.' . PHP_EOL); exit(1); } $curl = curl_init($url); curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2TLS, CURLOPT_TIMEOUT => 15, CURLOPT_USERAGENT => 'ExampleHttp2Client/1.0', ]); $body = curl_exec($curl); if ($body === false) { fwrite(STDERR, 'cURL error: ' . curl_error($curl) . PHP_EOL); curl_close($curl); exit(1); } $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $httpVersion = curl_getinfo($curl, CURLINFO_HTTP_VERSION); curl_close($curl); $versionNames = []; foreach ([ 'CURL_HTTP_VERSION_1_0' => 'HTTP/1.0', 'CURL_HTTP_VERSION_1_1' => 'HTTP/1.1', 'CURL_HTTP_VERSION_2_0' => 'HTTP/2', 'CURL_HTTP_VERSION_2' => 'HTTP/2', 'CURL_HTTP_VERSION_2TLS' => 'HTTP/2', ] as $constant => $label) { if (defined($constant)) { $versionNames[constant($constant)] = $label; } } $negotiated = $versionNames[$httpVersion] ?? 'unknown'; echo 'HTTP status: ' . $status . PHP_EOL; echo 'Negotiated protocol: ' . $negotiated . PHP_EOL; if ($status < 200 || $status >= 300 || $negotiated !== 'HTTP/2') { fwrite(STDERR, 'The request did not complete over HTTP/2.' . PHP_EOL); exit(1); }
The default URL points to an HTTPS endpoint that supports HTTP/2 for a repeatable client test. Replace it with the real API URL after the local PHP runtime proves HTTP/2 support.
- Run the script against an HTTPS endpoint that supports HTTP/2.
$ php http2-request.php https://nghttp2.org/httpbin/get cURL library: 8.18.0 HTTP/2 feature: yes HTTP status: 200 Negotiated protocol: HTTP/2
HTTP/2 feature: yes proves the PHP runtime's libcurl build can negotiate HTTP/2. Negotiated protocol: HTTP/2 proves this specific transfer used HTTP/2.
- Use the same option in the application request before curl_exec().
$curl = curl_init('https://api.example.com/v1/status'); curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2TLS, CURLOPT_TIMEOUT => 15, ]);
CURLOPT_HTTP_VERSION is a protocol request, not an absolute guarantee. Keep the CURLINFO_HTTP_VERSION check in diagnostics or smoke tests when the application depends on HTTP/2 behavior.
- Retest the real endpoint and read the negotiated protocol from curl_getinfo().
$ php http2-request.php https://api.example.com/v1/status
The output should still show HTTP/2 feature: yes and Negotiated protocol: HTTP/2. If the negotiated protocol is HTTP/1.1, the client request fell back. Check that the URL is HTTPS, the endpoint supports HTTP/2, no proxy is downgrading the request, and the PHP runtime is using the cURL build checked in the first script output.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.