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:

  1. 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;"
    true

    Install 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.