Files
jouwdriedee/.agents/skills/laravel-best-practices/rules/http-client.md
T
2026-10-31 14:23:53 +01:00

158 lines
4.4 KiB
Markdown

# 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:
```php
$response = Http::get('https://api.example.com/users');
```
Preferred:
```php
$response = Http::connectTimeout(3)
->timeout(5)
->get('https://api.example.com/users');
```
Define shared settings in a macro or a dedicated client:
```php
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:
```php
$response = Http::retry([100, 500, 1000])
->post('https://api.example.com/v1/charges', $data);
```
Safe for an idempotent request:
```php
$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:
```php
$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:
```php
$user = Http::get('https://api.example.com/users/1')->json();
```
Preferred:
```php
$user = Http::connectTimeout(3)
->timeout(5)
->get('https://api.example.com/users/1')
->throw()
->json();
```
When graceful degradation is required, handle expected alternatives explicitly:
```php
$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.
```php
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.
```php
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:
```php
Http::fake([
'api.example.com/*' => Http::failedConnection(),
]);
```