Files
Andreas Reinhold / reiniandClaude Opus 5 92b3b3de56 Regenerate Boost guidelines and skills
Generated by boost:update for Boost 2.8, which replaces the pest-testing
skill with testing-best-practices and adds infer-conventions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYnWFt9pJEwvAmNFN38XD
2026-09-10 10:42:22 +02: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. Remember that 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

The HTTP client returns responses for 4xx and 5xx status codes instead of throwing by default. 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();

Handle expected alternatives explicitly when graceful degradation is required:

$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, fake a connection failure when testing the integration's failure path:

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