Symfony 8.1 ships a batch of improvements for the HttpClient component that touch performance, interoperability, security and testing.
Persistent cURL Connections
When you make many requests in a row, re-establishing connections for every
request adds DNS, TCP and TLS overhead. PHP 8.5 introduces persistent cURL
handles and Symfony 8.1 exposes them through the new
extra.use_persistent_connections option of CurlHttpClient:
1 2 3 4 5 6 7
use Symfony\Component\HttpClient\CurlHttpClient;
$client = new CurlHttpClient([
'extra' => [
'use_persistent_connections' => true,
],
]);
When enabled (and running on PHP 8.5 or later), the DNS cache, SSL sessions and connection data are reused across requests, reducing the connection overhead. This is most useful for CLI commands and workers that make many sequential requests.
Using Symfony HttpClient as a Guzzle Handler
Some SDKs are built on top of Guzzle and don't allow replacing the underlying
HTTP client. Symfony 8.1 adds a GuzzleHttpHandler that lets those SDKs use
Symfony HttpClient as their transport layer, benefiting from features such as
retries, HTTP/2 support and scoped clients:
1 2 3 4 5 6
use GuzzleHttp\Client;
use Symfony\Component\HttpClient\GuzzleHttpHandler;
$guzzle = new Client(['handler' => new GuzzleHttpHandler()]);
$response = $guzzle->request('GET', 'https://symfony.com/versions.json');
By default, the handler creates a new HttpClient instance. You can also pass any
HttpClientInterface implementation to reuse the same configuration across
your application and third-party SDKs.
Allowing Specific Hosts in NoPrivateNetworkHttpClient
NoPrivateNetworkHttpClient helps protect applications from SSRF attacks
by blocking requests to private networks (10.0.0.0/8, 192.168.0.0/16,
etc.). Symfony 8.1 adds a third $allowList argument that lets you exempt
specific hosts (e.g. a local proxy) while continuing to block all other
private-network addresses:
1 2 3 4 5 6 7 8
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
$client = new NoPrivateNetworkHttpClient(
HttpClient::create(),
null, // null = block the default private subnets
'10.0.0.42', // allow this host (also accepts an array of IPs/CIDR subnets)
);
Safer Default for Cached Responses
CachingHttpClient stores responses according to the cache-control directives
returned by the server. When no cache directives are provided, responses could
previously remain cached indefinitely.
In Symfony 8.1, the maximum time-to-live now defaults to 86400 seconds (24
hours) instead of being unlimited. Server-provided TTLs are still respected,
but they are capped to this value to prevent stale cache entries from living
indefinitely. You can change the cap with the max_ttl option:
1 2 3 4 5 6 7 8
# config/packages/framework.yaml
framework:
http_client:
scoped_clients:
example.client:
base_uri: 'https://example.com'
caching:
max_ttl: 3600
Passing null to disable the cap is now deprecated; use a positive integer instead.
Failing Fast with max_connect_duration
The timeout option controls how long the client waits for activity on an
idle connection and max_duration caps the total request time. Neither
provides a strict deadline for establishing the connection.
Symfony 8.1 adds the max_connect_duration option, which limits the time
spent on DNS resolution, the TCP connection and the TLS handshake:
1 2 3 4 5
$response = $client->request('GET', 'https://...', [
// fail fast if the connection cannot be established within 0.5 seconds
'max_connect_duration' => 0.5,
'timeout' => 10,
]);
A value of 0 or less means there is no limit on connection time, as long as
the timeout option is respected.
Per-Client Mocking in Tests
When testing, you can set mock_response_factory to replace HttpClient with a
mock that returns canned responses. In Symfony 8.1, this option can now be
configured per scoped client, making it easier to mock specific APIs while
leaving others untouched.
Set it to true for a MockHttpClient that returns empty 200
responses, to false to disable mocking for a specific client, or to a
service ID for full control over the returned responses:
1 2 3 4 5 6 7 8 9 10 11
# config/packages/test/framework.yaml
framework:
http_client:
mock_response_factory: true
scoped_clients:
my_api.client:
base_uri: 'https://my-api.com'
mock_response_factory: App\Tests\MyApiMockClientCallback
not_mocked.client:
base_uri: 'https://example.com'
mock_response_factory: false
Custom DNS Resolution
Symfony 8.1 adds a DnsResolvingHttpClient decorator that lets you provide
custom DNS resolution logic (e.g. to route requests to a specific server while
debugging). The resolver receives the hostname and returns the IP address to
use, or null to fall back to the default transport resolution:
1 2 3 4 5 6 7 8 9
use Symfony\Component\HttpClient\DnsResolvingHttpClient;
use Symfony\Component\HttpClient\HttpClient;
$resolver = function (string $hostname): ?string {
// implement your own resolution logic (service discovery, caching, etc.)
return $ip; // or null to use the default DNS resolution
};
$client = new DnsResolvingHttpClient(HttpClient::create(), $resolver);