Symfony 8.1 ships a batch of improvements for the HttpClient component that touch performance, interoperability, security and testing.

Persistent cURL Connections

Kostiantyn Miakshyn
Contributed by Kostiantyn Miakshyn in #62751

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

Nicolas Grekas
Contributed by Nicolas Grekas in #63433

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

Pascal CESCON
Contributed by Pascal CESCON in #64160

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

Jérôme Parmentier Nicolas Grekas
Contributed by Jérôme Parmentier and Nicolas Grekas in #63441

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

Alexandre Daubois
Contributed by Alexandre Daubois in #62854

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

Tarjei Huse
Contributed by Tarjei Huse in #52265

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

Peter Potrowl
Contributed by Peter Potrowl in #63770

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);
Published in #Living on the edge