Files
2026-04-09 16:06:44 -06:00

3.6 KiB

HTTP Client Best Practices

Always Set Explicit Timeouts

The default timeout is 30 seconds — too long for most API calls. Always set explicit timeout and connectTimeout to fail fast.

Incorrect:

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

Correct:

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

For service-specific clients, define timeouts in a macro:

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

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

Use Retry with Backoff for External APIs

External APIs have transient failures. Use retry() with increasing delays.

Incorrect:

$response = Http::post('https://api.stripe.com/v1/charges', $data);

if ($response->failed()) {
    throw new PaymentFailedException('Charge failed');
}

Correct:

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

Only retry on specific errors:

$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {
    return $exception instanceof ConnectionException
        || ($exception instanceof RequestException && $exception->response->serverError());
})->post('https://api.example.com/data');

Handle Errors Explicitly

The HTTP Client does not throw on 4xx/5xx by default. Always check status or use throw().

Incorrect:

$response = Http::get('https://api.example.com/users/1');
$user = $response->json(); // Could be an error body

Correct:

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

$user = $response->json();

For graceful degradation:

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

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

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

$response->throw();

Use Request Pooling for Concurrent Requests

When making multiple independent API calls, use Http::pool() instead of sequential calls.

Incorrect:

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

Correct:

use Illuminate\Http\Client\Pool;

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

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

Fake HTTP Calls in Tests

Never make real HTTP requests in tests. Use Http::fake() and preventStrayRequests().

Incorrect:

it('syncs user from API', function () {
    $service = new UserSyncService;
    $service->sync(1); // Hits the real API
});

Correct:

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

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

    $service = new UserSyncService;
    $service->sync(1);

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

Test failure scenarios too:

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