Files
2026-10-31 14:23:53 +01:00

4.4 KiB

HTTP client best practices

Set explicit timeouts

Laravel's HTTP client has a 30-second response timeout by default. Choose response and connection timeouts that fit the service and the calling request or job. Retries can multiply the total elapsed time.

Less resilient:

$response = Http::get('https://api.example.com/users');

Preferred:

$response = Http::connectTimeout(3)
    ->timeout(5)
    ->get('https://api.example.com/users');

Define shared settings in a macro or a dedicated client:

Http::macro('github', function () {
    return Http::baseUrl('https://api.github.com')
        ->connectTimeout(3)
        ->timeout(10)
        ->withToken(config('services.github.token'));
});

$response = Http::github()->get('/repos/laravel/framework');

Retry only safe operations

Retry transient connection failures, rate-limit responses, and server errors with an appropriate delay. Retry idempotent requests such as GET when the operation can safely run more than once. Retry a state-changing request only when the remote API supports an idempotency key or provides equivalent duplicate protection.

Unsafe without an idempotency guarantee:

$response = Http::retry([100, 500, 1000])
    ->post('https://api.example.com/v1/charges', $data);

Safe for an idempotent request:

$response = Http::connectTimeout(3)
    ->timeout(10)
    ->retry([100, 500, 1000], 0, function (Throwable $exception) {
        return $exception instanceof ConnectionException
            || ($exception instanceof RequestException
                && ($exception->response->serverError() || $exception->response->status() === 429));
    })
    ->get('https://api.example.com/data');

For a supported state-changing API, send a stable idempotency key for every attempt:

$response = Http::withHeaders(['Idempotency-Key' => $paymentAttempt->uuid])
    ->connectTimeout(3)
    ->timeout(10)
    ->retry([100, 500, 1000], 0, function (Throwable $exception) {
        return $exception instanceof ConnectionException
            || ($exception instanceof RequestException
                && ($exception->response->serverError() || $exception->response->status() === 429));
    })
    ->post('https://api.example.com/v1/charges', $data);

Handle errors explicitly

By default, the HTTP client returns responses for 4xx and 5xx status codes instead of throwing. Inspect the expected statuses or call throw() before consuming a success payload.

Unsafe when a success payload is expected:

$user = Http::get('https://api.example.com/users/1')->json();

Preferred:

$user = Http::connectTimeout(3)
    ->timeout(5)
    ->get('https://api.example.com/users/1')
    ->throw()
    ->json();

When graceful degradation is required, handle expected alternatives explicitly:

$response = Http::connectTimeout(3)
    ->timeout(5)
    ->get('https://api.example.com/users/1');

if ($response->successful()) {
    return $response->json();
}

if ($response->notFound()) {
    return null;
}

$response->throw();

Pool independent requests

Use Http::pool() when several independent requests can run concurrently. Pooling changes execution time, not error handling. Inspect or throw for each response as needed.

use Illuminate\Http\Client\Pool;

$responses = Http::pool(fn (Pool $pool) => [
    $pool->as('users')->connectTimeout(3)->timeout(5)
        ->get('https://api.example.com/users'),
    $pool->as('posts')->connectTimeout(3)->timeout(5)
        ->get('https://api.example.com/posts'),
]);

$users = $responses['users']->throw()->json();
$posts = $responses['posts']->throw()->json();

Fake HTTP requests in tests

Use Http::fake() for external integrations, and use Http::preventStrayRequests() when an unexpected real request should fail the test. Also test timeouts, connection failures, and error responses that the application handles.

it('syncs a user from the API', function () {
    Http::preventStrayRequests();

    Http::fake([
        'api.example.com/users/1' => Http::response([
            'name' => 'John Doe',
            'email' => 'john@example.com',
        ]),
    ]);

    (new UserSyncService)->sync(1);

    Http::assertSent(fn (Request $request) =>
        $request->url() === 'https://api.example.com/users/1'
    );
});

For example, to test the integration's failure path, fake a connection failure:

Http::fake([
    'api.example.com/*' => Http::failedConnection(),
]);