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
158 lines
4.4 KiB
Markdown
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. Remember that 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
|
|
|
|
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:
|
|
|
|
```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();
|
|
```
|
|
|
|
Handle expected alternatives explicitly when graceful degradation is required:
|
|
|
|
```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, fake a connection failure when testing the integration's failure path:
|
|
|
|
```php
|
|
Http::fake([
|
|
'api.example.com/*' => Http::failedConnection(),
|
|
]);
|
|
```
|