Skip to content

A TLS failure is reported as a substr() type error, with no mention of the network #47

Description

@jwrosewell

What happens

HttpClient::makeCloudRequest calls substr() on the result of
curl_exec without checking for false, so any transport failure, a TLS
certificate problem in particular, surfaces as a type error about
substr() with no mention of the network or of TLS.

Reproduced on 5 September 2026, PHP 8.5.1 on Windows, running the TAC
lookup example from
device-detection-php
against https://cloud.51degrees.com on a machine where PHP has no
curl.cainfo set:

PHP Fatal error:  Uncaught TypeError: substr(): Argument #1 ($string) must be of type string, false given in ...\vendor\51degrees\fiftyone.pipeline.cloudrequestengine\src\HttpClient.php:86
Stack trace:
#0 ...\src\HttpClient.php(86): substr()
#1 ...\src\CloudRequestEngine.php(279): fiftyone\pipeline\cloudrequestengine\HttpClient->makeCloudRequest()
#2 ...\src\CloudRequestEngine.php(119): fiftyone\pipeline\cloudrequestengine\CloudRequestEngine->getEngineProperties()
#3 ...\vendor\51degrees\fiftyone.pipeline.engines\src\Engine.php(107): fiftyone\pipeline\cloudrequestengine\CloudRequestEngine->processInternal()
...
  thrown in ...\src\HttpClient.php on line 86

The actual cause, read back from the same handle:

$ php -r '$ch = curl_init("https://cloud.51degrees.com/api/v4/accessibleproperties");
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
          $d = curl_exec($ch);
          var_dump($d === false);
          echo "errno=" . curl_errno($ch) . " err=" . curl_error($ch) . PHP_EOL;'
bool(true)
errno=60 err=SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)

Nothing in what the person running the example sees points at the
certificate store, so the natural next step is to go looking for a bug in
the example or in the pipeline, which is the wrong place.

Where

src/HttpClient.php,
version 4.5.7 on Packagist, lines 79 to 86:

$data = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Get headers length
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
// Get response header and body using header length
$headerStr = substr($data, 0, $headerSize);

Suggested fix

Check curl_exec for false before using the result, and throw with
curl_errno and curl_error in the message, naming the URL that was being
requested. Close the handle on that path as well, since the current code
only reaches curl_close on success. Something of this shape:

$data = curl_exec($ch);

if ($data === false) {
    $errorNumber = curl_errno($ch);
    $errorText = curl_error($ch);
    curl_close($ch);

    throw new \Exception(
        'The request to ' . $url . ' could not be completed. curl ' .
        'reported error ' . $errorNumber . ': ' . $errorText
    );
}

A test can force the failure without a network by pointing the client at a
host that will not resolve, and asserting the message names curl and the
URL rather than substr.

The same check is worth applying to the file_get_contents branch above
it, which is used when curl is not available. @file_get_contents also
returns false on failure and the result is passed straight to
validateResponse.

Found by

Running the cloud examples in
device-detection-php
as part of the work in
device-detection-php#73,
which fixes the example side faults but cannot fix this one, because the
file is in this repository.


Produced with AI assistance. The output above is real rather than
illustrative, and the suggested fix has not been implemented or tested, so
it needs human review.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions