http clients and apis

cURL

cURL is PHP's lower-level HTTP client extension. It gives detailed control over methods, headers, bodies, redirects, TLS, timeouts, and response metadata.

cURL is common in older PHP applications, small integrations, and codebases that do not use a framework HTTP client. Even when you use Guzzle or Symfony HttpClient, understanding cURL helps with debugging because many clients use cURL underneath.

Configure a request handle

cURL requests start with a handle from curl_init(). You then set options and execute the handle.

PHP example
<?php

declare(strict_types=1);

if (!function_exists('curl_init')) {
    echo 'cURL extension unavailable' . PHP_EOL;
    return;
}

$ch = curl_init('https://api.example.test/products');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 5,
    CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);

echo 'cURL request configured' . PHP_EOL;
curl_close($ch);

// Prints:
// cURL request configured

CURLOPT_RETURNTRANSFER tells cURL to return the response body from curl_exec() instead of printing it. Most API clients should set it.

Send a JSON POST

For JSON APIs, encode the payload and set both Content-Type and Accept headers.

PHP example
<?php

declare(strict_types=1);

$payload = ['name' => 'Keyboard', 'price' => 99.99];
$json = json_encode($payload, JSON_THROW_ON_ERROR);

$headers = [
    'Accept: application/json',
    'Content-Type: application/json',
];

echo 'POST body: ' . $json . PHP_EOL;
echo 'Headers: ' . implode(', ', $headers) . PHP_EOL;

// Prints:
// POST body: {"name":"Keyboard","price":99.99}
// Headers: Accept: application/json, Content-Type: application/json

In real cURL code, those values become CURLOPT_POSTFIELDS and CURLOPT_HTTPHEADER.

Transport errors and HTTP errors are different

curl_exec() can fail because of a transport problem: DNS failure, timeout, TLS issue, refused connection, and similar errors.

An HTTP response with status 404 or 500 is not the same kind of failure. The server responded; it responded with an error status.

PHP example
<?php

declare(strict_types=1);

function classifyCurlResult(bool|string $body, int $statusCode, string $curlError): string
{
    if ($body === false) {
        return 'transport error: ' . $curlError;
    }

    if ($statusCode >= 400) {
        return 'http error: ' . $statusCode;
    }

    return 'success';
}

echo classifyCurlResult('{"ok":true}', 200, '') . PHP_EOL;
echo classifyCurlResult('Not found', 404, '') . PHP_EOL;
echo classifyCurlResult(false, 0, 'Operation timed out') . PHP_EOL;

// Prints:
// success
// http error: 404
// transport error: Operation timed out

Good API code handles both categories deliberately.

Timeouts are required

Always set timeouts for external calls. Without timeouts, your PHP workers can be tied up waiting on slow services.

Useful options include:

  • CURLOPT_CONNECTTIMEOUT: time allowed to establish the connection
  • CURLOPT_TIMEOUT: total request time
  • CURLOPT_RETURNTRANSFER: return the body instead of printing it
  • CURLOPT_HEADER: include response headers in the output if you need them
  • CURLOPT_HTTPHEADER: request headers
  • CURLOPT_CUSTOMREQUEST: method such as PATCH or DELETE

TLS verification

Do not disable TLS verification to "fix" certificate problems in production. Options like CURLOPT_SSL_VERIFYPEER => false make HTTPS much less meaningful.

If certificates fail, fix the certificate chain, CA bundle, environment, hostname, or proxy configuration.

A small request wrapper shape

This example avoids a real network call, but shows the result shape a wrapper should return:

PHP example
<?php

declare(strict_types=1);

/**
 * @return array{status: int, body: string, error: string|null}
 */
function fakeCurlResponse(bool $transportOk, int $status): array
{
    if (!$transportOk) {
        return ['status' => 0, 'body' => '', 'error' => 'Connection timed out'];
    }

    return ['status' => $status, 'body' => '{"ok":true}', 'error' => null];
}

$response = fakeCurlResponse(true, 200);

echo $response['status'] . ' ' . ($response['error'] ?? 'no transport error') . PHP_EOL;

// Prints:
// 200 no transport error

Returning status, body, and transport error separately makes the caller's decision clearer.

What to check in a project

Check every cURL request has connection and total timeouts.

Check transport errors are handled separately from HTTP status codes.

Check JSON requests set the correct headers and use json_encode().

Check secrets are sent in headers or bodies as the API expects, not logged or placed in URLs.

Check TLS verification has not been disabled.

Check whether repeated cURL setup should be moved into a small client class or replaced by a maintained HTTP client library.

What you should be able to do

After this lesson, you should be able to configure a cURL request, send JSON payloads, set timeouts, distinguish transport failures from HTTP error statuses, and spot unsafe TLS or logging choices.

Practice

Task: Classify cURL Outcomes

Write a small PHP script that classifies the outcome of a cURL-style request.

Requirements

  • Use declare(strict_types=1);.
  • Accept a response body, HTTP status code, and cURL error string.
  • Return success for a 2xx response with no transport error.
  • Return http_error for 4xx or 5xx statuses.
  • Return transport_error when the body is false.
  • Include all three cases in the output.

Check Your Work

Run the script and confirm a timeout is not treated the same as a 404 response.

Show solution

This solution models the decision you make after curl_exec().

PHP example
<?php

declare(strict_types=1);

function classifyCurlOutcome(bool|string $body, int $statusCode, string $curlError): string
{
    if ($body === false) {
        return 'transport_error: ' . $curlError;
    }

    if ($statusCode >= 400) {
        return 'http_error: ' . $statusCode;
    }

    return 'success: ' . $statusCode;
}

echo classifyCurlOutcome('{"ok":true}', 200, '') . PHP_EOL;
echo classifyCurlOutcome('Not found', 404, '') . PHP_EOL;
echo classifyCurlOutcome(false, 0, 'Operation timed out') . PHP_EOL;

// Prints:
// success: 200
// http_error: 404
// transport_error: Operation timed out

In real cURL code, $body comes from curl_exec($ch), the status comes from curl_getinfo($ch, CURLINFO_RESPONSE_CODE), and the error comes from curl_error($ch).

Why This Works

The examples separate network failure from server responses. That distinction matters for retries, alerts, logs, and user-facing error messages.