HttpClient et la requête lazy
La requête n'est envoyée qu'au premier accès au body.
Imagine que tu demandes à un serveur "envoie-moi le menu". Avec la plupart des clients HTTP, la requête part immédiatement. Avec le HttpClient de Symfony, c'est différent : il répond "d'accord, je note" — et n'envoie vraiment la requête qu'au moment où tu lis le résultat.
$client->request() retourne immédiatement un objet ResponseInterface sans attendre la réponse réseau. La connexion TCP, l'envoi et la réception n'ont lieu qu'au premier appel à getContent(), toArray() ou getStatusCode(). Cela permet de lancer plusieurs requêtes avant de les lire.
Sélectionner le bon client selon l'environnement :
use Symfony\Component\HttpClient\HttpClient; // Sélection automatique du meilleur client disponible : // → CurlHttpClient si ext-curl est chargée (HTTP/2, HTTP/3, concurrence native) // → NativeHttpClient sinon (stream wrappers PHP, portable) $client = HttpClient::create(); // La requête est enregistrée — aucune connexion TCP à ce stade $response = $client->request('GET', 'https://api.example.com/users/42');
Les trois méthodes de lecture — chacune déclenche la requête si elle n'a pas encore été envoyée :
// Récupère le body brut sous forme de string $body = $response->getContent(); // string // Décode le JSON et retourne un tableau PHP associatif $data = $response->toArray(); // array // Retourne le code HTTP (200, 404, 503…) $code = $response->getStatusCode(); // int
Les headers sont aussi accessibles :
// Retourne un tableau indexé par nom de header (en minuscules) // Chaque valeur est un tableau de strings (un header peut apparaître plusieurs fois) $headers = $response->getHeaders(); // array<string, list<string>> // Exemple : récupérer le Content-Type $type = $headers['content-type'][0] ?? '';
lazy, pas de réseau
toArray()
getStatusCode()
maintenant seulement
curl_multi_exec pour traiter plusieurs connexions simultanément sans threads.
$response->getInfo('url'), $response->getInfo('http_code'), $response->getInfo('total_time').
Explore le cycle de vie lazy d'une réponse — clique sur les étapes dans l'ordre.
Les options de requête
json, body, headers, auth_basic/auth_bearer, timeout, on_progress — une option par cas d'usage.
Chaque requête peut être configurée via un tableau d'options passé en troisième paramètre de request(). Voici les options les plus courantes, classées par cas d'usage.
Envoyer du JSON
// L'option 'json' encode automatiquement en JSON // et ajoute Content-Type: application/json $response = $client->request('POST', 'https://api.example.com/users', [ 'json' => [ 'name' => 'Alice', 'email' => 'alice@example.com', ], ]);
Envoyer un formulaire
// L'option 'body' avec un tableau envoie application/x-www-form-urlencoded // Avec une string ou un Traversable, c'est le body brut $response = $client->request('POST', 'https://auth.example.com/token', [ 'body' => [ 'grant_type' => 'client_credentials', 'client_id' => 'my-app', ], ]);
Authentification
// HTTP Basic Authentication $response = $client->request('GET', 'https://api.example.com/data', [ 'auth_basic' => ['user', 'secret'], // ou 'user:secret' ]); // Bearer Token (JWT, OAuth2…) $response = $client->request('GET', 'https://api.example.com/me', [ 'auth_bearer' => $token, // ajoute Authorization: Bearer <token> ]);
Headers et query string
$response = $client->request('GET', 'https://api.example.com/search', [ 'headers' => [ 'Accept' => 'application/json', 'X-API-Key' => 'abc123', ], 'query' => [ 'q' => 'symfony', 'page' => 2, ], // → ?q=symfony&page=2 ]);
Timeout et durée maximale
$response = $client->request('GET', 'https://slow-api.example.com/report', [ // timeout : délai d'inactivité réseau (en secondes, float) // Si aucun octet reçu pendant X secondes → TransportExceptionInterface 'timeout' => 5.0, // max_duration : durée totale maximale de la requête (0 = illimité) 'max_duration' => 30.0, ]);
Progression
$response = $client->request('GET', 'https://files.example.com/large.zip', [ // on_progress : callback appelé à chaque chunk reçu 'on_progress' => function (int $dlNow, int $dlSize, array $info): void { if ($dlSize > 0) { $pct = round($dlNow / $dlSize * 100); echo "\rTéléchargement : {$pct} %"; } }, ]);
'json' sérialise et pose Content-Type: application/json automatiquement. 'body' avec un tableau pose Content-Type: application/x-www-form-urlencoded. Avec une string, 'body' envoie le contenu brut sans toucher aux headers.
$client->withOptions([...]) — il retourne une nouvelle instance sans modifier l'originale.
| Option | Type | Défaut | Usage |
|---|---|---|---|
json | mixed | null | Body JSON — encode + Content-Type auto |
body | array|string|Traversable | '' | Body form ou brut |
headers | array | [] | Headers HTTP |
query | array | [] | Paramètres de query string |
auth_basic | string|array | null | Authentification Basic |
auth_bearer | string | null | Authentification Bearer |
timeout | float | null | Inactivité réseau (secondes) |
max_duration | float | 0 | Durée totale max (0 = illimité) |
max_redirects | int | 20 | Nombre max de redirections |
verify_peer | bool | true | Vérifier le certificat SSL |
on_progress | callable | null | Callback de progression |
buffer | bool | true | Bufferiser le body complet en mémoire |
Gérer les exceptions
4xx, 5xx, timeout, JSON invalide — chaque erreur a sa propre exception.
Le HttpClient de Symfony suit un principe clair : par défaut, toute réponse non-2xx lève une exception. Tu peux désactiver ce comportement en passant false aux méthodes de lecture.
Toutes héritent de
HttpExceptionInterface qui étend TransportExceptionInterface. La distinction clé : les exceptions transport surviennent avant d'avoir reçu une réponse HTTP (timeout, DNS, TLS). Les exceptions HTTP surviennent après — la réponse est reçue mais son code indique une erreur.
| Exception | Déclencheur |
|---|---|
TransportExceptionInterface |
Erreur réseau : timeout, DNS, TLS, connexion refusée |
ClientExceptionInterface |
Réponse 4xx (400, 401, 403, 404…) |
ServerExceptionInterface |
Réponse 5xx (500, 502, 503…) |
RedirectionExceptionInterface |
Redirection 3xx non résolue (max_redirects dépassé) |
DecodingExceptionInterface |
Body non JSON valide lors de toArray() |
Exemple complet de gestion d'erreurs :
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; try { $response = $client->request('GET', 'https://api.example.com/users/42'); $data = $response->toArray(); // déclenche les exceptions HTTP et JSON } catch (ClientExceptionInterface $e) { // Erreur côté client : 404, 403, 401… // La réponse est accessible via $e->getResponse() $code = $e->getResponse()->getStatusCode(); echo "Erreur client {$code}"; } catch (ServerExceptionInterface $e) { // Erreur serveur : 500, 503… echo "Erreur serveur, réessaie plus tard"; } catch (TransportExceptionInterface $e) { // Problème réseau : DNS, TLS, timeout, connexion refusée echo "Problème réseau : {$e->getMessage()}"; } catch (DecodingExceptionInterface $e) { // Le body reçu n'est pas du JSON valide echo "Réponse JSON invalide"; }
Pour ignorer les codes d'erreur et lire la réponse quand même :
$response = $client->request('GET', 'https://api.example.com/unknown'); // false = ne pas lancer d'exception sur les codes d'erreur $code = $response->getStatusCode(); // lit le code sans exception $body = $response->getContent(false); // lit le body, même si 4xx/5xx $data = $response->toArray(false); // décode sans lever ClientExceptionInterface if ($code === 404) { // gestion manuelle }
getStatusCode() est la seule méthode qui ne lance jamais d'exception HTTP (4xx/5xx). Elle peut en revanche lancer TransportExceptionInterface si la connexion réseau elle-même a échoué.
ClientExceptionInterface, ServerExceptionInterface et RedirectionExceptionInterface exposent toutes getResponse(), ce qui permet d'inspecter le body de la réponse d'erreur (message d'API, détail de l'erreur…).
Streaming — traiter les réponses volumineuses
stream() retourne un itérateur de ChunkInterface — on traite le body au fur et à mesure.
Télécharger un fichier de 500 Mo avec getContent() charge tout en mémoire. Le streaming permet de traiter le body fragment par fragment — empreinte mémoire constante quelle que soit la taille de la réponse.
$client->stream($response) retourne un ResponseStreamInterface, un itérateur qui produit des paires (ResponseInterface, ChunkInterface). Chaque chunk représente un fragment du body — ou un événement spécial (headers reçus, fin du body, timeout).
Lecture d'un fichier volumineux en streaming :
// buffer:false est indispensable — sinon Symfony bufferise quand même $response = $client->request('GET', 'https://files.example.com/data.jsonl', [ 'buffer' => false, ]); $fh = fopen('/tmp/data.jsonl', 'wb'); foreach ($client->stream($response) as $chunk) { if ($chunk->isFirst()) { // Les headers sont arrivés — on peut lire le status code // mais pas encore de body $code = $response->getStatusCode(); if ($code !== 200) break; } if ($chunk->isLast()) { // Body complet reçu break; } if ($chunk->isTimeout()) { // Aucun octet reçu pendant le délai 'timeout' continue; } // Écriture du fragment sur disque fwrite($fh, $chunk->getContent()); echo "Offset : {$chunk->getOffset()} octets reçus\n"; } fclose($fh);
Les méthodes clés de ChunkInterface :
| Méthode | Description |
|---|---|
isFirst() | Premier chunk — les headers sont disponibles |
isLast() | Dernier chunk — le body est complet |
isTimeout() | Aucune donnée reçue pendant le délai timeout |
getContent() | Fragment de body reçu (string, peut être vide) |
getOffset() | Position en octets dans le stream total |
stream(). Sans 'buffer' => false, tu ne bénéficies pas du streaming — toute la réponse est déjà en RAM dès le premier chunk.
EventSourceHttpClient (un décorateur) simplifie la lecture de flux SSE. Il gère le parsing du protocole text/event-stream et expose les events directement via stream().
Clique sur Démarrer le stream pour simuler la réception de chunks.
Concurrence — plusieurs requêtes en parallèle
stream() sur un tableau de réponses traite chaque requête dès qu'elle a des données — pas d'attente séquentielle.
Imagine que tu dois récupérer 10 ressources d'une API. En séquentiel, tu attends chaque réponse avant d'envoyer la suivante — c'est 10× le temps d'une requête. Avec le HttpClient de Symfony, tu lances toutes les requêtes, puis stream() traite chaque réponse dès qu'elle a des données disponibles.
$client->stream(iterable $responses) accepte un tableau de ResponseInterface. CurlHttpClient multiplex les connexions via curl_multi_exec — plusieurs requêtes progressent simultanément sur les mêmes threads PHP.
Pattern classique — fan-out + collect :
// 1. Lancer toutes les requêtes (aucune n'a encore bloqué) $responses = []; foreach ([1, 2, 3, 4, 5] as $id) { $responses[$id] = $client->request('GET', "https://api.example.com/users/{$id}"); } // 2. stream() traite chaque réponse dès qu'elle a des données // L'ordre de traitement n'est PAS garanti — c'est la plus rapide qui sort en premier foreach ($client->stream($responses) as $response => $chunk) { if (!$chunk->isLast()) { continue; // ignore les chunks intermédiaires } // Body complet reçu — identifier la requête via getInfo() $url = $response->getInfo('url'); $data = $response->toArray(); echo "Reçu {$url} : {$data['name']}\n"; }
Utiliser getInfo() pour identifier la réponse dans le contexte de la boucle :
// user_data permet d'attacher n'importe quelle donnée à la requête // pour la retrouver dans stream() $responses = []; foreach ($userIds as $id) { $responses[] = $client->request('GET', "/users/{$id}", [ 'user_data' => ['id' => $id], ]); } foreach ($client->stream($responses) as $response => $chunk) { if (!$chunk->isLast()) continue; $meta = $response->getInfo('user_data'); echo "User #{$meta['id']} reçu\n"; }
Annuler une requête pendant le streaming :
foreach ($client->stream($responses) as $response => $chunk) { if ($chunk->isFirst() && $response->getStatusCode() === 404) { // Annule la connexion — libère les ressources cURL immédiatement $response->cancel(); } }
aucun réseau encore
curl_multi_exec
$response => $chunk
traite dès réception
curl_multi_exec dans le même thread PHP. Ce n'est pas du multithreading — c'est de l'I/O multiplexée. L'ordre de retour dépend du réseau, pas de l'ordre d'envoi.
$client->stream($responses, 1.5) — le second paramètre est un timeout en secondes. Si aucune réponse n'a de données pendant ce délai, la boucle produit un chunk avec isTimeout() === true.
Scoped clients — options par base URI
ScopingHttpClient applique des options par défaut à un scope d'URLs — withOptions() retourne une nouvelle instance.
Au lieu de répéter 'auth_bearer' => $token, 'base_uri' => 'https://api.example.com' à chaque requête, tu peux créer un client scopé qui applique automatiquement ces options à toutes ses requêtes.
Décorateur qui applique des options par défaut aux requêtes dont l'URL correspond à un pattern. Plusieurs scopes peuvent coexister dans le même client.
withOptions() retourne une nouvelle instance sans modifier l'originale.
Créer un client scopé avec HttpClient::createForBaseUri() :
use Symfony\Component\HttpClient\HttpClient; // Toutes les requêtes relatives seront résolues contre cette base URI // Les options s'appliquent uniquement aux URLs qui commencent par la base URI $apiClient = HttpClient::createForBaseUri('https://api.example.com', [ 'auth_bearer' => $token, 'timeout' => 10.0, 'headers' => ['Accept' => 'application/json'], ]); // Plus besoin de répéter auth_bearer et timeout $user = $apiClient->request('GET', '/users/42')->toArray(); $orders = $apiClient->request('GET', '/orders?user_id=42')->toArray();
Utiliser ScopingHttpClient directement pour plusieurs scopes :
use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpClient\HttpClient; $client = ScopingHttpClient::forBaseUri( HttpClient::create(), 'https://api.github.com', [ 'auth_bearer' => $githubToken, 'headers' => ['Accept' => 'application/vnd.github+json'], ] ); // Construction avancée : plusieurs regexp → options différentes $multiClient = new ScopingHttpClient( HttpClient::create(), [ 'https://api\.github\.com' => ['auth_bearer' => $githubToken], 'https://api\.stripe\.com' => ['auth_basic' => $stripeKey], ], // regexp par défaut (quand aucune ne match) 'https://api\.github\.com' );
Créer un client dérivé avec des options supplémentaires via withOptions() :
// withOptions() retourne une NOUVELLE instance — n'affecte pas $apiClient $uploadClient = $apiClient->withOptions([ 'timeout' => 120.0, // timeout plus long pour les uploads 'max_duration' => 300.0, ]); // $apiClient conserve timeout:10.0 — $uploadClient a timeout:120.0
Configuration YAML — clients nommés autowirés :
# config/packages/framework.yaml framework: http_client: clients: github_client: base_uri: 'https://api.github.com' auth_bearer: '%env(GITHUB_TOKEN)%' timeout: 10 stripe_client: base_uri: 'https://api.stripe.com' auth_basic: '%env(STRIPE_SECRET_KEY)%'
// Injection par attribut — Symfony 6.3+ use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Component\DependencyInjection\Attribute\Target; public function __construct( #[Target('github_client')] private readonly HttpClientInterface $github, #[Target('stripe_client')] private readonly HttpClientInterface $stripe, ) {}
request() fusionnent avec les options par défaut du client scopé, elles ne les remplacent pas. Un header ajouté à request() s'ajoute aux headers du scope.
RetryableHttpClient — retry automatique
RetryableHttpClient retente automatiquement sur les erreurs transitoires — codes HTTP, délai exponentiel, jitter.
Certaines erreurs sont transitoires : un 503 temporaire, un 429 (rate limit), une coupure réseau. Relancer la requête quelques instants plus tard suffit souvent à résoudre le problème. RetryableHttpClient automatise ce comportement.
Décorateur qui retente automatiquement les requêtes échouées selon une stratégie configurable. Par défaut : 3 tentatives maximum, délai exponentiel (1s, 2s, 4s) avec jitter de 10 % pour éviter les "thundering herds".
Usage minimal avec les valeurs par défaut :
use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\RetryableHttpClient; // 3 tentatives max, stratégie par défaut (429, 500, 502, 503, 504…) $client = new RetryableHttpClient(HttpClient::create());
Configuration avancée avec GenericRetryStrategy :
use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; $strategy = new GenericRetryStrategy( // codes HTTP qui déclenchent un retry (0 = erreur transport) statusCodes: [0, 429, 500, 502, 503, 504], // délai initial en millisecondes delayMs: 500, // multiplicateur exponentiel (500ms → 1000ms → 2000ms…) multiplier: 2.0, // délai maximum quelle que soit l'exponentielle (0 = illimité) maxDelayMs: 10_000, // jitter : variation aléatoire de ±10 % du délai jitter: 0.1, ); $client = new RetryableHttpClient( client: HttpClient::create(), strategy: $strategy, maxRetries: 5, );
Les codes HTTP retentés par défaut :
| Code | Signification | Note |
|---|---|---|
0 | Erreur transport (DNS, TLS, timeout) | Toutes méthodes |
429 | Too Many Requests | Toutes méthodes |
500 | Internal Server Error | Méthodes idempotentes seulement |
502 | Bad Gateway | Toutes méthodes |
503 | Service Unavailable | Toutes méthodes |
504 | Gateway Timeout | Toutes méthodes |
507 | Insufficient Storage | Toutes méthodes |
510 | Not Extended | Toutes méthodes |
GenericRetryStrategy ne retente que si la méthode est idempotente, pour éviter les effets de bord (double paiement, double insertion…).
Configuration YAML avec retry_failed :
# config/packages/framework.yaml framework: http_client: clients: api_client: base_uri: 'https://api.example.com' retry_failed: max_retries: 3 delay: 1000 multiplier: 2.0 jitter: 0.1 http_codes: [429, 500, 503]
Implémenter une stratégie personnalisée — par exemple pour respecter le header Retry-After :
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\ResponseInterface; final class RetryAfterStrategy implements RetryStrategyInterface { public function shouldRetry( ResponseInterface $response, ?string $responseContent, ?TransportExceptionInterface $exception ): ?bool { return $response->getStatusCode() === 429; } public function getDelay( ResponseInterface $response, ?string $responseContent, ?TransportExceptionInterface $exception ): int { // Respecte le délai demandé par l'API (en secondes → millisecondes) $retryAfter = $response->getHeaders(false)['retry-after'][0] ?? null; return $retryAfter !== null ? (int) $retryAfter * 1000 : 5000; // fallback : 5 secondes } }
MockHttpClient — tester sans réseau
MockHttpClient remplace le vrai client dans les tests — aucune requête réseau.
Tester du code qui fait des requêtes HTTP sans dépendre du réseau ni d'un serveur externe, c'est possible avec MockHttpClient. Il remplace le vrai client — même interface, aucune connexion réelle.
Implémentation de
HttpClientInterface qui retourne des réponses prédéfinies sans effectuer de requêtes réseau. Utilisé dans les tests unitaires et d'intégration pour contrôler exactement ce que le client retourne.
Réponse statique avec MockResponse :
use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; // Une seule réponse JSON $mock = new MockHttpClient( new MockResponse( json_encode(['id' => 42, 'name' => 'Alice']), ['http_code' => 200, 'response_headers' => ['Content-Type: application/json']] ) ); $data = $mock->request('GET', '/users/42')->toArray(); // $data = ['id' => 42, 'name' => 'Alice']
Plusieurs réponses consécutives (une par requête) :
$mock = new MockHttpClient([ new MockResponse('{"status":"processing"}', ['http_code' => 202]), new MockResponse('{"status":"done"}', ['http_code' => 200]), ]); $r1 = $mock->request('POST', '/jobs')->toArray(); // ['status' => 'processing'] $r2 = $mock->request('GET', '/jobs/1')->toArray(); // ['status' => 'done']
Réponse dynamique avec une factory callable :
// La factory reçoit la méthode, l'URL et les options de chaque requête // Permet de valider les paramètres et de retourner une réponse conditionnelle $mock = new MockHttpClient(function (string $method, string $url, array $options): MockResponse { // Vérifier que la requête est correctement formée if (!str_contains($url, '/users/')) { return new MockResponse('', ['http_code' => 404]); } $id = basename($url); return new MockResponse( json_encode(['id' => (int) $id, 'name' => "User {$id}"]), ['http_code' => 200] ); });
Inspecter les requêtes envoyées depuis le code testé :
$responses = [ new MockResponse('{"created":true}', ['http_code' => 201]), ]; $mock = new MockHttpClient($responses, 'https://api.example.com'); // … exécuter le code testé qui utilise $mock … // Inspecter la requête qui a été envoyée $sentRequest = $responses[0]; assertEquals('POST', $sentRequest->getRequestMethod()); assertEquals('https://api.example.com/users', $sentRequest->getRequestUrl()); $body = json_decode($sentRequest->getRequestOptions()['body'], true); assertEquals('Alice', $body['name']);
Simuler un body streamé (utile pour tester stream()) :
// MockResponse accepte un Traversable pour simuler des chunks $chunks = ['{"id":1}', "\n", '{"id":2}', "\n"]; $mock = new MockHttpClient( new MockResponse((function () use ($chunks) { yield from $chunks; })()) );
MockResponse::fromFile('/path/to/fixture.json', ['http_code' => 200]).
MockHttpClient dans config/packages/test/framework.yaml ou via $this->getContainer()->set(HttpClientInterface::class, $mock) dans un KernelTestCase.
Les décorateurs HttpClient
HttpClient suit le patron décorateur — chaque wrapper ajoute une fonctionnalité sans modifier le client de base.
Le HttpClient de Symfony est conçu autour du patron décorateur : un client de base (CurlHttpClient ou NativeHttpClient) qui implémente HttpClientInterface, encapsulé dans des wrappers qui ajoutent des fonctionnalités sans toucher au code du client de base ni à ton code applicatif.
Chaque décorateur implémente la même interface que le composant de base et le délègue. Tu peux empiler autant de décorateurs que nécessaire — l'ordre compte. Le code client ne sait pas dans combien de couches la requête passe.
HttpClientInterface
requête réelle
Vue d'ensemble de tous les décorateurs disponibles :
| Décorateur | Fonctionnalité ajoutée |
|---|---|
ScopingHttpClient |
Options par défaut par base URI ou regexp |
RetryableHttpClient |
Retry automatique sur erreurs transitoires |
CachingHttpClient |
Cache RFC 9111 — GET/HEAD uniquement |
ThrottlingHttpClient |
Rate limiting via RateLimiterInterface |
NoPrivateNetworkHttpClient |
Bloque les requêtes vers IPs privées (sécurité SSRF) |
EventSourceHttpClient |
Parsing Server-Sent Events (SSE) |
TraceableHttpClient |
Profiling — enregistre toutes les requêtes |
MockHttpClient |
Fausses réponses — tests sans réseau |
CachingHttpClient — cache HTTP standard :
use Symfony\Component\HttpClient\CachingHttpClient; use Symfony\Component\HttpKernel\HttpCache\Store; // Store : répertoire de stockage du cache (fichiers) $store = new Store(__DIR__ . '/cache/http'); $client = new CachingHttpClient(HttpClient::create(), $store); // Requête mise en cache si la réponse a des headers de cache valides // (Cache-Control: max-age, ETag, Last-Modified…) $response = $client->request('GET', 'https://api.example.com/config');
ThrottlingHttpClient — rate limiting :
use Symfony\Component\HttpClient\ThrottlingHttpClient; use Symfony\Component\RateLimiter\RateLimiterFactory; // RateLimiterFactory configuré via le composant symfony/rate-limiter $limiter = $factory->create('github-api'); $client = new ThrottlingHttpClient(HttpClient::create(), $limiter); // Chaque request() attend la disponibilité d'un token avant d'envoyer
NoPrivateNetworkHttpClient — protection SSRF :
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; // Bloque les requêtes vers : 127.x.x.x, 10.x.x.x, 192.168.x.x, // 172.16.x.x–172.31.x.x, ::1, fc00::/7, link-local… // Indispensable si l'URL provient d'une entrée utilisateur $safeClient = new NoPrivateNetworkHttpClient(HttpClient::create());
TraceableHttpClient — profiling :
use Symfony\Component\HttpClient\TraceableHttpClient; // Utilisé automatiquement par le Profiler Symfony en mode dev/test $client = new TraceableHttpClient(HttpClient::create()); $client->request('GET', 'https://api.example.com/data'); // Inspecter toutes les requêtes tracées foreach ($client->getTracedRequests() as $req) { echo "{$req['method']} {$req['url']} → {$req['info']['http_code']}\n"; }
RetryableHttpClient à l'extérieur de CachingHttpClient — tu veux retenter si le cache miss renvoie une erreur réseau, pas retenter depuis le cache. Pour NoPrivateNetworkHttpClient, il doit être le plus proche du réseau (intérieur de la pile).
Synthèse — le guide de choix
Quel client, quelles options, quel décorateur — le guide de décision.
Un seul HttpClientInterface, plusieurs implémentations et décorateurs. Le bon choix dépend du contexte.
Choisir le bon client de base
| Client | Quand l'utiliser |
|---|---|
HttpClient::create() |
Toujours — sélection automatique du meilleur disponible |
CurlHttpClient |
Production : HTTP/2, concurrence native, HTTP/3 si ext-curl récente |
NativeHttpClient |
Environnements sans ext-curl (containers légers, scripts CLI) |
MockHttpClient |
Tests unitaires et d'intégration uniquement |
Choisir les bonnes options
| Besoin | Option |
|---|---|
| Envoyer du JSON | 'json' => $data |
| Envoyer un formulaire | 'body' => ['key' => 'value'] |
| Authentification API | 'auth_bearer' => $token |
| Authentification Basic | 'auth_basic' => ['user', 'pass'] |
| Paramètres URL | 'query' => ['page' => 2] |
| Timeout réseau | 'timeout' => 5.0 |
| Streaming sans mémoire | 'buffer' => false + stream() |
| Suivi progression | 'on_progress' => fn(…) => … |
Choisir le bon décorateur
| Besoin | Décorateur |
|---|---|
| Options par défaut par API | ScopingHttpClient |
| Retry automatique | RetryableHttpClient |
| Cache RFC 9111 | CachingHttpClient |
| Rate limiting | ThrottlingHttpClient |
| Sécurité SSRF (URL user) | NoPrivateNetworkHttpClient |
| Server-Sent Events | EventSourceHttpClient |
| Profiling/debug | TraceableHttpClient |
Gérer les erreurs
Par défaut — toArray() et getContent() lèvent une exception sur 4xx/5xx. Attrape ClientExceptionInterface, ServerExceptionInterface, TransportExceptionInterface.
try { $data = $response->toArray(); } catch (ClientExceptionInterface $e) { // 4xx }
Passe false pour inspecter manuellement le status code — utile quand une réponse 404 est un comportement attendu.
$code = $response->getStatusCode(); $body = $response->getContent(false); if ($code === 404) { ... }
Pièges à éviter
- Oublier
buffer: falseavant de streamer — le body est déjà en mémoire, aucun bénéfice. - Empiler les
toArray()séquentiels sur plusieurs réponses — utilisestream()pour les traiter en parallèle. - Retenter les POST avec
GenericRetryStrategysans adapter lesstatusCodes— risque d'effets de bord. - Ne pas protéger les URLs utilisateur avec
NoPrivateNetworkHttpClient— risque de SSRF. - Partager un client scopé entre services avec des credentials différents — utilise
withOptions()pour dériver des instances séparées.
NoPrivateNetworkHttpClient (sécurité) → CachingHttpClient (cache) → RetryableHttpClient (résilience) → ScopingHttpClient (base URI + auth) → CurlHttpClient (réseau).