guzzle — Cursor rule: what it does, how to install
Guides Cursor to write clean PHP HTTP integrations: factories with timeouts, retry middleware, and PSR-18 compliance.
.cursor/rules/guzzle.mdcWhat guzzle does
The rule stops the assistant from scattering bare new Client() calls across PHP code. It enforces injecting configured clients through factory services. Every instance must define explicit values for timeout and connect_timeout, alongside a shared base_uri. Without explicit timeouts, requests can hang indefinitely under network failures.
Shared behavior belongs in the HandlerStack. The rule directs Cursor to register GuzzleHttp\Middleware for cross-cutting tasks. Retry logic limits attempts to 3 retries with backoff starting at 100 ms for connection drops and 5xx server responses. Authentication headers are mapped centrally via Middleware::mapRequest instead of repeating them in every service method.
For public libraries and SDK packages, the rule forbids a direct Guzzle dependency. It requires type-hinting PSR-18 ClientInterface and PSR-17 factory interfaces so consumers can supply any HTTP client. Error handling requires catching specific exceptions like RequestException and ConnectException.
What it helps with
- configure a centralized Guzzle client factory with connection timeouts
- set up retry middleware with exponential backoff on 5xx responses
- attach request logging via HandlerStack
- decouple SDK code using PSR-18 and PSR-17 interfaces
Example prompts for the agent
- Create a Guzzle client factory with a 5 second connect timeout and retry middleware
- Refactor this API service to use HandlerStack for bearer token authentication
- Update our SDK client to accept a PSR-18 ClientInterface instead of Guzzle directly
How to install guzzle
Manually: put the files in place
.cursor/rules/guzzle.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/guzzle.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/guzzle.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/guzzle.mdManually: put the files in place
.clinerules/guzzle.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about guzzle
How to install guzzle in Cursor?
Add the rule file at .cursor/rules/guzzle.mdc in your project root. Cursor will automatically apply it whenever you work on **/*.php files.
What does the guzzle rule do?
It enforces standard Guzzle practices: defining timeouts, structuring middleware for retries and auth, and keeping clients testable.
Why does the rule discourage direct Guzzle dependencies in SDKs?
Hard dependencies lock package consumers into Guzzle. The rule instructs the model to rely on PSR-18 and PSR-17 interfaces so callers can inject their own client.
Source text
guzzle.mdc · CC0-1.0
---
description: Definitive guidelines for using Guzzle HTTP client in PHP, focusing on robust client configuration, effective middleware, PSR-18 interoperability, and testability.
globs: **/*.php
---
# Guzzle Best Practices
Guzzle is the de-facto PHP HTTP client. Use these rules to build robust, testable, and maintainable HTTP integrations.
## 1. Client Instantiation & Configuration
Always inject an immutable Guzzle client as a service. Configure it once with global defaults, especially timeouts.
### ✅ GOOD: Service-Oriented Client with Essential Defaults
```php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Log\LoggerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\ConnectException;
use Exception;
final class MyApiClientFactory
{
public static function create(LoggerInterface $logger): Client
{
$stack = HandlerStack::create();
// 1. Logging: Log all requests and responses
$stack->push(Middleware::log(
$logger,
new \GuzzleHttp\MessageFormatter(
'{method} {uri} HTTP/{version} {code} {res_header_Content-Length}'
)
));
// 2. Retry Logic: Robust handling for transient errors (5xx, connection issues)
$stack->push(Middleware::retry(
function (
$retries,
RequestInterface $request,
ResponseInterface $response = null,
Exception $exception = null
) {
// Limit to 3 retries
if ($retries >= 3) {
return false;
}
// Retry on connection exceptions
if ($exception instanceof ConnectException) {
return true;
}
// Retry on server errors (5xx)
if ($response && $response->getStatusCode() >= 500) {
return true;
}
return false;
},
// Exponential backoff: 100ms, 200ms, 400ms
function ($retries) {
return 100 * (2 ** ($retries - 1));
}
));
return new Client([
'base_uri' => 'https://api.example.com/v1/',
'timeout' => 10.0, // Total request timeout in seconds
'connect_timeout' => 5.0, // Connection timeout in seconds
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'MyApp/1.0 (PHP Guzzle)',
],
'handler' => $stack,
'http_errors' => true, // Throw exceptions for 4xx/5xx responses
]);
}
}
// Usage in a service container or dependency injection
// $client = MyApiClientFactory::create($myLogger);
// $response = $client->get('users');
```
### ❌ BAD: Global Singletons & Missing Timeouts
```php
<?php
use GuzzleHttp\Client;
// Client created ad-hoc, no shared configuration, no timeouts
// Hard to test, prone to hanging requests
class LegacyService
{
public function fetchUsers(): array
{
$client = new Client(); // ❌ BAD: No timeouts, no base_uri, no shared config
$response = $client->get('https://api.example.com/v1/users');
return json_decode($response->getBody()->getContents(), true);
}
}
```
## 2. Middleware for Cross-Cutting Concerns
Leverage `HandlerStack` and `GuzzleHttp\Middleware` for reusable logic like retries, logging, and authentication. This keeps your request code DRY.
### ✅ GOOD: Centralized Middleware for Auth & Retries
(See `MyApiClientFactory::create` example above for comprehensive middleware usage.)
```php
<?php
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
// Example of adding an authentication header via middleware
$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
$token = 'your_api_token_here'; // Get from secure config
return $request->withHeader('Authorization', 'Bearer ' . $token);
}));
```
### ❌ BAD: Duplicating Logic Per Request
```php
<?php
use GuzzleHttp\Client;
class UserService
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function getUser(string $id): array
{
// ❌ BAD: Authentication header added manually for every request type
$response = $this->client->get("users/{$id}", [
'headers' => [
'Authorization' => 'Bearer ' . 'your_api_token_here',
],
]);
return json_decode($response->getBody()->getContents(), true);
}
}
```
## 3. "Bring Your Own Client" (BYOC) for Libraries/SDKs
When building reusable libraries or SDKs, **never hard-depend on Guzzle**. Instead, type-hint `Psr\Http\Client\ClientInterface` (PSR-18) and `Psr\Http\Message\RequestFactoryInterface` (PSR-17). Let the consumer inject their preferred HTTP client.
### ✅ GOOD: PSR-18/17 Compliant SDK
```php
<?php
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\ResponseInterface;
final class MySdkClient
{
private const string BASE_URI = 'https://api.myservice.com';
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
) {}
public function fetchData(string $endpoint, array $data = []): ResponseInterface
{
$body = $this->streamFactory->createStream(json_encode($data));
$request = $this->requestFactory->createRequest('POST', self::BASE_URI . $endpoint)
->withHeader('Content-Type', 'application/json')
->withBody($body);
return $this->httpClient->sendRequest($request);
}
}
// Consumer code (e.g., in a Symfony/Laravel app)
// use GuzzleHttp\Psr7\HttpFactory;
// use GuzzleHttp\Client as GuzzleClient;
// $guzzleClient = new GuzzleClient(/* ... config ... */);
// $httpFactory = new HttpFactory(); // PSR-17 implementation
// $sdk = new MySdkClient($guzzleClient, $httpFactory, $httpFactory);
```
### ❌ BAD: Hard-Dependence on Guzzle in SDKs
```php
<?php
use GuzzleHttp\Client; // ❌ BAD: Direct Guzzle dependency
final class MyBadSdkClient
{
private Client $httpClient;
public function __construct(Client $httpClient) // ❌ BAD: Forces Guzzle on consumers
{
$this->httpClient = $httpClient;
}
public function fetchData(string $endpoint): array
{
$response = $this->httpClient->get('https://api.myservice.com' . $endpoint);
return json_decode($response->getBody()->getContents(), true);
}
}
```
## 4. Error Handling
Always catch specific Guzzle exceptions, especially `RequestException` (for HTTP errors) and `ConnectException` (for network issues).
### ✅ GOOD: Specific Exception Handling
```php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
final class DataFetcher
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function fetchImportantData(): ?array
{
try {
$response = $this->client->get('critical-endpoint');
return json_decode($response->getBody()->getContents(), true);
} catch (ConnectException $e) {
// Log network error, notify ops, potentially retry (if not handled by middleware)
error_log("Network error fetching data: " . $e->getMessage());
return null; // Or throw a custom application exception
} catch (RequestException $e) {
// Log HTTP error (4xx, 5xx), inspect response
error_log("HTTP error fetching data: " . $e->getMessage() . " Response: " . $e->getResponse()?->getBody());
if ($e->hasResponse() && $e->getResponse()->getStatusCode() === 404) {
return []; // Handle specific 404 case
}
return null; // Or throw a custom application exception
} catch (\Throwable $e) {
// Catch any other unexpected errors
error_log("Unexpected error: " . $e->getMessage());
throw $e; // Re-throw for higher-level handling
}
}
}
```
### ❌ BAD: Catching Generic Exceptions
```php
<?php
// ... (class definition)
public function fetchImportantData(): ?array
{
try {
$response = $this->client->get('critical-endpoint');
return json_decode($response->getBody()->getContents(), true);
} catch (\Exception $e) { // ❌ BAD: Catches too broadly, hides specific issues
error_log("Error fetching data: " . $e->getMessage());
return null;
}
}
```
## 5. Testing Guzzle Clients
Use `GuzzleHttp\Handler\MockHandler` for deterministic and fast unit tests. This avoids hitting real APIs and ensures consistent test results.
### ✅ GOOD: Mocking Responses with `MockHandler`
```php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
final class MyServiceTest extends TestCase
{
public function testFetchUsersSuccessfully(): void
{
// Create a mock handler with a successful response
$mock = new MockHandler([
new Response(200, ['Content-Type' => 'application/json'], json_encode(['users' => ['Alice', 'Bob']])),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
// Inject the mocked client into your service
$service = new MyService($client);
$users = $service->fetchUsers();
$this->assertEquals(['Alice', 'Bob'], $users);
}
public function testFetchUsersNotFound(): void
{
// Mock a 404 response
$mock = new MockHandler([
new Response(404, [], 'Not Found'),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$service = new MyService($client);
// Expect an exception or specific return based on your error handling
$this->expectException(\GuzzleHttp\Exception\ClientException::class); // If http_errors is true
$service->fetchUsers();
}
}
// Example MyService class
class MyService
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function fetchUsers(): array
{
$response = $this->client->get('users');
return json_decode($response->getBody()->getContents(), true)['users'];
}
}
```
### ❌ BAD: Hitting Real APIs in Tests
```php
<?php
use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;
final class MyServiceIntegrationTest extends TestCase
{
public function testFetchUsersFromRealApi(): void
{
// ❌ BAD: Hits a real external API, making tests slow, flaky, and dependent on external services
$client = new Client(['base_uri' => 'https://api.example.com/v1/']);
$service = new MyService($client);
$users = $service->fetchUsers();
$this->assertIsArray($users);
$this->assertNotEmpty($users);
}
}
```
## 6. Laravel HTTP Client
If you are in a Laravel project, use Laravel's built-in HTTP client wrapper. It's powered by Guzzle but provides a more fluent API, built-in faking, and concurrent request helpers.
### ✅ GOOD: Laravel HTTP Client
```php
<?php
use Illuminate\Support\Facades\Http;
final class LaravelIntegrationService
{
public function getUserData(int $userId): array
{
$response = Http::withToken('your_api_token')
->timeout(10)
->retry(3, 100) // Retry 3 times, 100ms delay
->get("https://api.example.com/users/{$userId}");
$response->throw(); // Throws an exception for 4xx/5xx responses
return $response->json();
}
public function sendConcurrentRequests(): array
{
$responses = Http::pool(fn (Http\Client\Pool $pool) => [
$pool->get('https://api.example.com/data/1'),
$pool->get('https://api.example.com/data/2'),
]);
return [
$responses[0]->json(),
$responses[1]->json(),
];
}
}
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc