Skip to content
Muhammet Şafak
tr
Web Development 4 min read

Consuming Third-Party APIs: HTTP Client with Guzzle

Connecting to external services in PHP with the Guzzle HTTP client — covering timeouts, error handling, and building a reliable request structure.


As an application grows, connecting to external services becomes inevitable. Payment providers, SMS gateways, weather APIs, shipment tracking services — if each of these integrations is built on its own pile of curl_init() calls, understanding how any of them behaves eventually becomes a real challenge.

Guzzle is an HTTP client library for PHP. It supports PSR-7-compliant messages, handles both synchronous and asynchronous requests, and provides a consistent error handling model. Frameworks like Laravel and Symfony already depend on it — chances are it’s already installed in your project.

Installation

If it isn’t installed yet, adding it via Composer is all it takes:

composer require guzzlehttp/guzzle

A Simple GET Request

use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.example.com',
]);

$response = $client->get('/users/42');
$data = json_decode($response->getBody(), true);

The base_uri parameter means you don’t have to repeat the full URL on every request. I appreciate this small detail on integrations where all requests go to the same base address.

Setting Timeouts

External services don’t always respond quickly. Making a request without a timeout can leave your server waiting indefinitely for a response. In any critical flow, that’s unacceptable.

$client = new Client([
    'base_uri'        => 'https://api.example.com',
    'timeout'         => 5.0,   // throws an exception if no response within 5 seconds
    'connect_timeout' => 2.0,   // maximum time allowed to establish the connection
]);

There are two distinct timeouts: connect_timeout limits how long it takes to reach the server, while timeout limits how long to wait for a response after the connection is established. Setting both is good practice.

When choosing timeout values, think about the “worst normal scenario.” If a payment provider typically responds in 800 ms, a 5-second timeout is reasonable. But if you set it to 30 seconds, users will sit staring at the screen for half a minute whenever that service slows down. I learned this the hard way with a shipment integration: there was no default timeout set, and the day that service got sluggish, every order page locked up.

Error Handling

Guzzle throws exceptions by default for 4xx and 5xx responses. You need to catch them with try/catch:

use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\RequestException;

try {
    $response = $client->post('/orders', [
        'json' => ['product_id' => 5, 'quantity' => 2],
    ]);
} catch (ClientException $e) {
    // 4xx: client error (invalid request, unauthorized, etc.)
    $statusCode = $e->getResponse()->getStatusCode();
    // Log it or return an appropriate message to the user
} catch (ServerException $e) {
    // 5xx: error on the external service's side
    // Retry logic can be evaluated here
} catch (RequestException $e) {
    // Connection error, timeout, and other network-level issues
}

Each exception type carries a different meaning, and distinguishing between them matters. 4xx errors usually indicate that the data your application sent was incorrect; 5xx errors point to a problem on the external service’s end. Treating them the same way can be misleading.

There’s another trap worth knowing: some APIs always return HTTP 200 and write error information into the response body instead of using proper status codes. In that case, Guzzle won’t throw an exception — you have to parse the response and check it yourself. Before starting an integration, read the service’s documentation for its error behavior; otherwise you’ll end up silently processing a response that came back as 200 but contained {"success": false} inside.

Headers and Authentication

Most APIs require a token or API key for authentication:

$client = new Client([
    'base_uri' => 'https://api.example.com',
    'headers'  => [
        'Authorization' => 'Bearer ' . config('services.example.token'),
        'Accept'        => 'application/json',
    ],
]);

Pulling sensitive values like tokens from your .env file rather than hardcoding them is important. Leaving a plaintext token in source code is an easy mistake to make and a late one to catch.

Wrapping It in a Service Class

Rather than instantiating Guzzle with new Client() everywhere, writing a dedicated service class for each integration keeps things organized:

class ExampleApiService
{
    private Client $client;

    public function __construct()
    {
        $this->client = new Client([
            'base_uri' => config('services.example.base_uri'),
            'timeout'  => 5.0,
            'headers'  => [
                'Authorization' => 'Bearer ' . config('services.example.token'),
            ],
        ]);
    }

    public function getUser(int $id): array
    {
        $response = $this->client->get("/users/{$id}");
        return json_decode($response->getBody(), true);
    }
}

With this structure, if you ever swap Guzzle for another HTTP library or need to use a mock client, the change stays confined to a single class.

Validating the Response

When an API claims it returns a specific format, checking that format rather than blindly trusting it is good practice. Failing early when an expected key is missing beats an unexpected null error surfacing hours later:

public function getUser(int $id): array
{
    $response = $this->client->get("/users/{$id}");
    $data = json_decode($response->getBody(), true);

    if (!isset($data['id'], $data['email'])) {
        throw new \UnexpectedValueException(
            "API returned an unexpected format: " . $response->getBody()
        );
    }

    return $data;
}

The most commonly overlooked detail when integrating external services is timeout values. If every integration is set up from the start with a sensible timeout and proper error handling, the rest of your application stays unaffected when that service eventually slows down or becomes unreachable. After the freeze I experienced with the shipment integration, I added timeout configuration and response validation as defaults to my service class template for every new integration.

Tags: #PHP
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind