HTTP Client
Warning: You are browsing the documentation for Symfony 4.x, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
4.3
The HttpClient component was introduced in Symfony 4.3.
Installation
The HttpClient component is a low-level HTTP client with support for both PHP stream wrappers and cURL. It provides utilities to consume APIs and supports synchronous and asynchronous operations. You can install it with:
1
$ composer require symfony/http-client
Basic Usage
Use the HttpClient class to make
requests. In the Symfony framework, this class is available as the
http_client
service. This service will be autowired
automatically when type-hinting for HttpClientInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
use Symfony\Contracts\HttpClient\HttpClientInterface;
class SymfonyDocs
{
private $client;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
public function fetchGitHubInformation(): array
{
$response = $this->client->request(
'GET',
'https://api.github.com/repos/symfony/symfony-docs'
);
$statusCode = $response->getStatusCode();
// $statusCode = 200
$contentType = $response->getHeaders()['content-type'][0];
// $contentType = 'application/json'
$content = $response->getContent();
// $content = '{"id":521583, "name":"symfony-docs", ...}'
$content = $response->toArray();
// $content = ['id' => 521583, 'name' => 'symfony-docs', ...]
return $content;
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13
use Symfony\Component\HttpClient\HttpClient;
$client = HttpClient::create();
$response = $client->request('GET', 'https://api.github.com/repos/symfony/symfony-docs');
$statusCode = $response->getStatusCode();
// $statusCode = 200
$contentType = $response->getHeaders()['content-type'][0];
// $contentType = 'application/json'
$content = $response->getContent();
// $content = '{"id":521583, "name":"symfony-docs", ...}'
$content = $response->toArray();
// $content = ['id' => 521583, 'name' => 'symfony-docs', ...]
Tip
The HTTP client is interoperable with many common HTTP client abstractions in PHP. You can also use any of these abstractions to profit from autowirings. See Interoperability for more information.
Configuration
The HTTP client contains many options you might need to take full control of the way the request is performed, including DNS pre-resolution, SSL parameters, public key pinning, etc. They can be defined globally in the configuration (to apply it to all requests) and to each request (which overrides any global configuration).
You can configure the global options using the default_options
option:
1 2 3 4 5
# config/packages/framework.yaml
framework:
http_client:
default_options:
max_redirects: 7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!-- config/packages/framework.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:http-client>
<framework:default-options max-redirects="7"/>
</framework:http-client>
</framework:config>
</container>
1 2 3 4 5 6 7 8
// config/packages/framework.php
$container->loadFromExtension('framework', [
'http_client' => [
'default_options' => [
'max_redirects' => 7,
],
],
]);
1 2 3
$client = HttpClient::create([
'max_redirects' => 7,
]);
Some options are described in this guide:
Check out the full http_client config reference to learn about all the options.
The HTTP client also has one configuration option called
max_host_connections
, this option can not be overridden by a request:
1 2 3 4 5
# config/packages/framework.yaml
framework:
http_client:
max_host_connections: 10
# ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!-- config/packages/framework.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:http-client max-host-connections="10">
<!-- ... -->
</framework:http-client>
</framework:config>
</container>
1 2 3 4 5 6 7
// config/packages/framework.php
$container->loadFromExtension('framework', [
'http_client' => [
'max_host_connections' => 10,
// ...
],
]);
1
$client = HttpClient::create([], 10);
Scoping Client
It's common that some of the HTTP client options depend on the URL of the request (e.g. you must set some headers when making requests to GitHub API but not for other hosts). If that's your case, the component provides scoped clients (using ScopingHttpClient) to autoconfigure the HTTP client based on the requested URL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# config/packages/framework.yaml
framework:
http_client:
scoped_clients:
# only requests matching scope will use these options
github.client:
scope: 'https://api\.github\.com'
headers:
Accept: 'application/vnd.github.v3+json'
Authorization: 'token %env(GITHUB_API_TOKEN)%'
# ...
# using base_uri, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs"))
# will default to these options
github.client:
base_uri: 'https://api.github.com'
headers:
Accept: 'application/vnd.github.v3+json'
Authorization: 'token %env(GITHUB_API_TOKEN)%'
# ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
<!-- config/packages/framework.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:http-client>
<!-- only requests matching scope will use these options -->
<framework:scoped-client name="github.client"
scope="https://api\.github\.com"
>
<framework:header name="Accept">application/vnd.github.v3+json</framework:header>
<framework:header name="Authorization">token %env(GITHUB_API_TOKEN)%</framework:header>
</framework:scoped-client>
<!-- using base-uri, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs"))
will default to these options -->
<framework:scoped-client name="github.client"
base-uri="https://api.github.com"
>
<framework:header name="Accept">application/vnd.github.v3+json</framework:header>
<framework:header name="Authorization">token %env(GITHUB_API_TOKEN)%</framework:header>
</framework:scoped-client>
</framework:http-client>
</framework:config>
</container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// config/packages/framework.php
$container->loadFromExtension('framework', [
'http_client' => [
'scoped_clients' => [
// only requests matching scope will use these options
'github.client' => [
'scope' => 'https://api\.github\.com',
'headers' => [
'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'token %env(GITHUB_API_TOKEN)%',
],
// ...
],
// using base_url, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs"))
// will default to these options
'github.client' => [
'base_uri' => 'https://api.github.com',
'headers' => [
'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'token %env(GITHUB_API_TOKEN)%',
],
// ...
],
],
],
]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\ScopingHttpClient;
$client = HttpClient::create();
$client = new ScopingHttpClient($client, [
// the options defined as values apply only to the URLs matching
// the regular expressions defined as keys
'https://api\.github\.com/' => [
'headers' => [
'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'token '.$githubToken,
],
],
// ...
]);
// relative URLs will use the 2nd argument as base URI and use the options of the 3rd argument
$client = ScopingHttpClient::forBaseUri($client, 'https://api.github.com/', [
'headers' => [
'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'token '.$githubToken,
],
]);
You can define several scopes, so that each set of options is added only if a
requested URL matches one of the regular expressions set by the scope
option.
If you use scoped clients in the Symfony framework, you must use any of the methods defined by Symfony to choose a specific service. Each client has a unique service named after its configuration.
Each scoped client also defines a corresponding named autowiring alias.
If you use for example
Symfony\Contracts\HttpClient\HttpClientInterface $githubClient
as the type and name of an argument, autowiring will inject the github.client
service into your autowired classes.
Note
Read the base_uri option docs to learn the rules applied when merging relative URIs into the base URI of the scoped client.
Making Requests
The HTTP client provides a single request()
method to perform all kinds of
HTTP requests:
1 2 3 4 5 6 7 8 9 10 11
$response = $client->request('GET', 'https://...');
$response = $client->request('POST', 'https://...');
$response = $client->request('PUT', 'https://...');
// ...
// you can add request options (or override global ones) using the 3rd argument
$response = $client->request('GET', 'https://...', [
'headers' => [
'Accept' => 'application/json',
],
]);
Responses are always asynchronous, so that the call to the method returns immediately instead of waiting to receive the response:
1 2 3 4 5 6 7 8 9
// code execution continues immediately; it doesn't wait to receive the response
$response = $client->request('GET', 'http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso');
// getting the response headers waits until they arrive
$contentType = $response->getHeaders()['content-type'][0];
// trying to get the response content will block the execution until
// the full response content is received
$content = $response->getContent();
This component also supports streaming responses for full asynchronous applications.
Authentication
The HTTP client supports different authentication mechanisms. They can be defined globally in the configuration (to apply it to all requests) and to each request (which overrides any global authentication):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/framework.yaml
framework:
http_client:
scoped_clients:
example_api:
base_uri: 'https://example.com/'
# HTTP Basic authentication
auth_basic: 'the-username:the-password'
# HTTP Bearer authentication (also called token authentication)
auth_bearer: the-bearer-token
# Microsoft NTLM authentication
auth_ntlm: 'the-username:the-password'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
<!-- config/packages/framework.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:http-client>
<!-- Available authentication options:
auth-basic: HTTP Basic authentication
auth-bearer: HTTP Bearer authentication (also called token authentication)
auth-ntlm: Microsoft NTLM authentication -->
<framework:scoped-client name="example_api"
base-uri="https://example.com/"
auth-basic="the-username:the-password"
auth-bearer="the-bearer-token"
auth-ntlm="the-username:the-password"
/>
</framework:http-client>
</framework:config>
</container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// config/packages/framework.php
$container->loadFromExtension('framework', [
'http_client' => [
'scoped_clients' => [
'example_api' => [
'base_uri' => 'https://example.com/',
// HTTP Basic authentication
'auth_basic' => 'the-username:the-password',
// HTTP Bearer authentication (also called token authentication)
'auth_bearer' => 'the-bearer-token',
// Microsoft NTLM authentication
'auth_ntlm' => 'the-username:the-password',
],
],
],
]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$client = HttpClient::createForBaseUri('https://example.com/', [
// HTTP Basic authentication (there are multiple ways of configuring it)
'auth_basic' => ['the-username'],
'auth_basic' => ['the-username', 'the-password'],
'auth_basic' => 'the-username:the-password',
// HTTP Bearer authentication (also called token authentication)
'auth_bearer' => 'the-bearer-token',
// Microsoft NTLM authentication (there are multiple ways of configuring it)
'auth_ntlm' => ['the-username'],
'auth_ntlm' => ['the-username', 'the-password'],
'auth_ntlm' => 'the-username:the-password',
]);
1 2 3 4 5 6
$response = $client->request('GET', 'https://...', [
// use a different HTTP Basic authentication only for this request
'auth_basic' => ['the-username', 'the-password'],
// ...
]);
Note
The NTLM authentication mechanism requires using the cURL transport.
By using HttpClient::createForBaseUri()
, we ensure that the auth credentials
won't be sent to any other hosts than https://example.com/.
4.4
The auth_ntlm
option and the HttpClient::createForBaseUri()
method were introduced in Symfony 4.4.
Query String Parameters
You can either append them manually to the requested URL, or define them as an
associative array via the query
option, that will be merged with the URL:
1 2 3 4 5 6 7 8
// it makes an HTTP GET request to https://httpbin.org/get?token=...&name=...
$response = $client->request('GET', 'https://httpbin.org/get', [
// these values are automatically encoded before including them in the URL
'query' => [
'token' => '...',
'name' => '...',
],
]);
Headers
Use the headers
option to define the default headers added to all requests:
1 2 3 4 5 6
# config/packages/framework.yaml
framework:
http_client:
default_options:
headers:
'User-Agent': 'My Fancy App'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<!-- config/packages/framework.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:http-client>
<framework:default-options>
<framework:header name="User-Agent">My Fancy App</framework:header>
</framework:default-options>
</framework:http-client>
</framework:config>
</container>
1 2 3 4 5 6 7 8 9 10
// config/packages/framework.php
$container->loadFromExtension('framework', [
'http_client' => [
'default_options' => [
'headers' => [
'User-Agent' => 'My Fancy App',
],
],
],
]);
1 2 3 4 5 6
// this header is added to all requests made by this client
$client = HttpClient::create([
'headers' => [
'User-Agent' => 'My Fancy App',
],
]);
You can also set new headers or override the default ones for specific requests:
1 2 3 4 5 6 7
// this header is only included in this request and overrides the value
// of the same header if defined globally by the HTTP client
$response = $client->request('POST', 'https://...', [
'headers' => [
'Content-Type' => 'text/plain',
],
]);
Uploading Data
This component provides several methods for uploading data using the body
option. You can use regular strings, closures, iterables and resources and they'll be
processed automatically when making the requests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
$response = $client->request('POST', 'https://...', [
// defining data using a regular string
'body' => 'raw data',
// defining data using an array of parameters
'body' => ['parameter1' => 'value1', '...'],
// using a closure to generate the uploaded data
'body' => function (int $size): string {
// ...
},
// using a resource to get the data from it
'body' => fopen('/path/to/file', 'r'),
]);
When uploading data with the POST
method, if you don't define the
Content-Type
HTTP header explicitly, Symfony assumes that you're uploading
form data and adds the required
'Content-Type: application/x-www-form-urlencoded'
header for you.
When the body
option is set as a closure, it will be called several times until
it returns the empty string, which signals the end of the body. Each time, the
closure should return a string smaller than the amount requested as argument.
A generator or any Traversable
can also be used instead of a closure.
Tip
When uploading JSON payloads, use the json
option instead of body
. The
given content will be JSON-encoded automatically and the request will add the
Content-Type: application/json
automatically too:
1 2 3 4 5
$response = $client->request('POST', 'https://...', [
'json' => ['param1' => 'value1', '...'],
]);
$decodedPayload = $response->toArray();
To submit a form with file uploads, it is your responsibility to encode the body
according to the multipart/form-data
content-type. The
Symfony Mime component makes it a few lines of code:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
$formFields = [
'regular_field' => 'some value',
'file_field' => DataPart::fromPath('/path/to/uploaded/file'),
];
$formData = new FormDataPart($formFields);
$client->request('POST', 'https://...', [
'headers' => $formData->getPreparedHeaders()->toArray(),
'body' => $formData->bodyToIterable(),
]);
By default, HttpClient streams the body contents when uploading them. This might
not work with all servers, resulting in HTTP status code 411 ("Length Required")
because there is no Content-Length
header. The solution is to turn the body
into a string with the following method (which will increase memory consumption
when the streams are large):
1 2 3 4
$client->request('POST', 'https://...', [
// ...
'body' => $formData->bodyToString(),
]);
If you need to add a custom HTTP header to the upload, you can do:
1 2
$headers = $formData->getPreparedHeaders()->toArray();
$headers[] = 'X-Foo: bar';
Cookies
The HTTP client provided by this component is stateless but handling cookies requires a stateful storage (because responses can update cookies and they must be used for subsequent requests). That's why this component doesn't handle cookies automatically.
You can either handle cookies yourself using the Cookie
HTTP header or use
the BrowserKit component which provides this
feature and integrates seamlessly with the HttpClient component.
Redirects
By default, the HTTP client follows redirects, up to a maximum of 20, when
making a request. Use the max_redirects
setting to configure this behavior
(if the number of redirects is higher than the configured value, you'll get a
RedirectionException):
1 2 3 4
$response = $client->request('GET', 'https://...', [
// 0 means to not follow any redirect
'max_redirects' => 0,
]);
HTTP Proxies
By default, this component honors the standard environment variables that your Operating System defines to direct the HTTP traffic through your local proxy. This means there is usually nothing to configure to have the client work with proxies, provided these env vars are properly configured.
You can still set or override these settings using the proxy
and no_proxy
options:
proxy
should be set to thehttp://...
URL of the proxy to get throughno_proxy
disables the proxy for a comma-separated list of hosts that do not require it to get reached.
Progress Callback
By providing a callable to the on_progress
option, one can track
uploads/downloads as they complete. This callback is guaranteed to be called on
DNS resolution, on arrival of headers and on completion; additionally it is
called when new data is uploaded or downloaded and at least once per second:
1 2 3 4 5 6 7
$response = $client->request('GET', 'https://...', [
'on_progress' => function (int $dlNow, int $dlSize, array $info): void {
// $dlNow is the number of bytes downloaded so far
// $dlSize is the total size to be downloaded or -1 if it is unknown
// $info is what $response->getInfo() would return at this very time
},
]);
Any exceptions thrown from the callback will be wrapped in an instance of
TransportExceptionInterface
and will abort the request.
HTTPS Certificates
HttpClient uses the system's certificate store to validate SSL certificates (while browsers use their own stores). When using self-signed certificates during development, it's recommended to create your own certificate authority (CA) and add it to your system's store.
Alternatively, you can also disable verify_host
and verify_peer
(see
http_client config reference), but this is not
recommended in production.
Performance
The component is built for maximum HTTP performance. By design, it is compatible with HTTP/2 and with doing concurrent asynchronous streamed and multiplexed requests/responses. Even when doing regular synchronous calls, this design allows keeping connections to remote hosts open between requests, improving performance by saving repetitive DNS resolution, SSL negotiation, etc. To leverage all these design benefits, the cURL extension is needed.
Enabling cURL Support
This component supports both the native PHP streams and cURL to make the HTTP requests. Although both are interchangeable and provide the same features, including concurrent requests, HTTP/2 is only supported when using cURL.
HttpClient::create()
selects the cURL transport if the cURL PHP extension
is enabled and falls back to PHP streams otherwise. If you prefer to select
the transport explicitly, use the following classes to create the client:
1 2 3 4 5 6 7 8
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\NativeHttpClient;
// uses native PHP streams
$client = new NativeHttpClient();
// uses the cURL PHP extension
$client = new CurlHttpClient();
When using this component in a full-stack Symfony application, this behavior is not configurable and cURL will be used automatically if the cURL PHP extension is installed and enabled. Otherwise, the native PHP streams will be used.
HTTP Compression
The HTTP header Accept-Encoding: gzip
is added automatically if:
- When using cURL client: cURL was compiled with ZLib support (see
php --ri curl
) - When using the native HTTP client: Zlib PHP extension is installed
If the server does respond with a gzipped response, it's decoded transparently.
To disable HTTP compression, send an Accept-Encoding: identity
HTTP header.
Chunked transfer encoding is enabled automatically if both your PHP runtime and the remote server supports it.
HTTP/2 Support
When requesting an https
URL, HTTP/2 is enabled by default if libcurl >= 7.36
is used. To force HTTP/2 for http
URLs, you need to enable it explicitly via
the http_version
option:
1 2 3 4 5
# config/packages/framework.yaml
framework:
http_client:
default_options:
http_version: '2.0'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!-- config/packages/framework.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:http-client>
<framework:default-options http-version="2.0"/>
</framework:http-client>
</framework:config>
</container>
1 2 3 4 5 6 7 8
// config/packages/framework.php
$container->loadFromExtension('framework', [
'http_client' => [
'default_options' => [
'http_version' => '2.0',
],
],
]);
1
$client = HttpClient::create(['http_version' => '2.0']);
Support for HTTP/2 PUSH works out of the box when libcurl >= 7.61 is used with PHP >= 7.2.17 / 7.3.4: pushed responses are put into a temporary cache and are used when a subsequent request is triggered for the corresponding URLs.
Processing Responses
The response returned by all HTTP clients is an object of type ResponseInterface which provides the following methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
$response = $client->request('GET', 'https://...');
// gets the HTTP status code of the response
$statusCode = $response->getStatusCode();
// gets the HTTP headers as string[][] with the header names lower-cased
$headers = $response->getHeaders();
// gets the response body as a string
$content = $response->getContent();
// casts the response JSON content to a PHP array
$content = $response->toArray();
// casts the response content to a PHP stream resource
$content = $response->toStream();
// cancels the request/response
$response->cancel();
// returns info coming from the transport layer, such as "response_headers",
// "redirect_count", "start_time", "redirect_url", etc.
$httpInfo = $response->getInfo();
// you can get individual info too
$startTime = $response->getInfo('start_time');
// e.g. this returns the final response URL (resolving redirections if needed)
$url = $response->getInfo('url');
// returns detailed logs about the requests and responses of the HTTP transaction
$httpLogs = $response->getInfo('debug');
4.4
The toStream()
method was introduced in Symfony 4.4.
Note
$response->getInfo()
is non-blocking: it returns live information
about the response. Some of them might not be known yet (e.g. http_code
)
when you'll call it.
Streaming Responses
Call the stream()
method of the HTTP client to get chunks of the
response sequentially instead of waiting for the entire response:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$url = 'https://releases.ubuntu.com/18.04.1/ubuntu-18.04.1-desktop-amd64.iso';
$response = $client->request('GET', $url);
// Responses are lazy: this code is executed as soon as headers are received
if (200 !== $response->getStatusCode()) {
throw new \Exception('...');
}
// get the response content in chunks and save them in a file
// response chunks implement Symfony\Contracts\HttpClient\ChunkInterface
$fileHandler = fopen('/ubuntu.iso', 'w');
foreach ($client->stream($response) as $chunk) {
fwrite($fileHandler, $chunk->getContent());
}
Note
By default, text/*
, JSON and XML response bodies are buffered in a local
php://temp
stream. You can control this behavior by using the buffer
option: set it to true
/false
to enable/disable buffering, or to a
closure that should return the same based on the response headers it receives
as an argument.
Canceling Responses
To abort a request (e.g. because it didn't complete in due time, or you want to
fetch only the first bytes of the response, etc.), you can either use the
cancel()
method of ResponseInterface
:
1
$response->cancel();
Or throw an exception from a progress callback:
1 2 3 4 5 6 7
$response = $client->request('GET', 'https://...', [
'on_progress' => function (int $dlNow, int $dlSize, array $info): void {
// ...
throw new \MyException();
},
]);
The exception will be wrapped in an instance of TransportExceptionInterface
and will abort the request.
In case the response was canceled using $response->cancel()
,
$response->getInfo('canceled')
will return true
.
4.4
The possibility to get information about canceled or not was introduced in Symfony 4.4.
Handling Exceptions
There are three types of exceptions, all of which implement the ExceptionInterface:
- Exceptions implementing the HttpExceptionInterface are thrown when your code does not handle the status codes in the 300-599 range.
- Exceptions implementing the TransportExceptionInterface are thrown when a lower level issue occurs.
- Exceptions implementing the DecodingExceptionInterface are thrown when a content-type cannot be decoded to the expected representation.
When the HTTP status code of the response is in the 300-599 range (i.e. 3xx,
4xx or 5xx), the getHeaders()
, getContent()
and toArray()
methods
throw an appropriate exception, all of which implement the
HttpExceptionInterface.
To opt-out from this exception and deal with 300-599 status codes on your own,
pass false
as the optional argument to every call of those methods,
e.g. $response->getHeaders(false);
.
If you do not call any of these 3 methods at all, the exception will still be thrown
when the $response
object is destructed.
Calling $response->getStatusCode()
is enough to disable this behavior
(but then don't miss checking the status code yourself).
While responses are lazy, their destructor will always wait for headers to come back. This means that the following request will complete; and if e.g. a 404 is returned, an exception will be thrown:
1 2 3 4
// because the returned value is not assigned to a variable, the destructor
// of the returned response will be called immediately and will throw if the
// status code is in the 300-599 range
$client->request('POST', 'https://...');
This in turn means that unassigned responses will fallback to synchronous requests. If you want to make these requests concurrent, you can store their corresponding responses in an array:
1 2 3 4 5 6 7 8
$responses[] = $client->request('POST', 'https://.../path1');
$responses[] = $client->request('POST', 'https://.../path2');
// ...
// This line will trigger the destructor of all responses stored in the array;
// they will complete concurrently and an exception will be thrown in case a
// status code in the 300-599 range is returned
unset($responses);
This behavior provided at destruction-time is part of the fail-safe design of the
component. No errors will be unnoticed: if you don't write the code to handle
errors, exceptions will notify you when needed. On the other hand, if you write
the error-handling code (by calling $response->getStatusCode()
), you will
opt-out from these fallback mechanisms as the destructor won't have anything
remaining to do.
Concurrent Requests
Thanks to responses being lazy, requests are always managed concurrently. On a fast enough network, the following code makes 379 requests in less than half a second when cURL is used:
1 2 3 4 5 6 7 8 9 10
$responses = [];
for ($i = 0; $i < 379; ++$i) {
$uri = "https://http2.akamai.com/demo/tile-$i.png";
$responses[] = $client->request('GET', $uri);
}
foreach ($responses as $response) {
$content = $response->getContent();
// ...
}
As you can read in the first "for" loop, requests are issued but are not consumed yet. That's the trick when concurrency is desired: requests should be sent first and be read later on. This will allow the client to monitor all pending requests while your code waits for a specific one, as done in each iteration of the above "foreach" loop.
Note
The maximum number of concurrent requests that you can perform depends on the resources of your machine (e.g. your operating system may limit the number of simultaneous reads of the file that stores the certificates file). Make your requests in batches to avoid these issues.
Multiplexing Responses
If you look again at the snippet above, responses are read in requests' order. But maybe the 2nd response came back before the 1st? Fully asynchronous operations require being able to deal with the responses in whatever order they come back.
In order to do so, the stream()
method of HTTP clients accepts a list of
responses to monitor. As mentioned previously,
this method yields response chunks as they arrive from the network. By replacing
the "foreach" in the snippet with this one, the code becomes fully async:
1 2 3 4 5 6 7 8 9 10 11 12
foreach ($client->stream($responses) as $response => $chunk) {
if ($chunk->isFirst()) {
// headers of $response just arrived
// $response->getHeaders() is now a non-blocking call
} elseif ($chunk->isLast()) {
// the full content of $response just completed
// $response->getContent() is now a non-blocking call
} else {
// $chunk->getContent() will return a piece
// of the response body that just arrived
}
}
Tip
Use the user_data
option combined with $response->getInfo('user_data')
to track the identity of the responses in your foreach loops.
Dealing with Network Timeouts
This component allows dealing with both request and response timeouts.
A timeout can happen when e.g. DNS resolution takes too much time, when the TCP
connection cannot be opened in the given time budget, or when the response
content pauses for too long. This can be configured with the timeout
request
option:
1 2 3
// A TransportExceptionInterface will be issued if nothing
// happens for 2.5 seconds when accessing from the $response
$response = $client->request('GET', 'https://...', ['timeout' => 2.5]);
The default_socket_timeout
PHP ini setting is used if the option is not set.
The option can be overridden by using the 2nd argument of the stream()
method.
This allows monitoring several responses at once and applying the timeout to all
of them in a group. If all responses become inactive for the given duration, the
method will yield a special chunk whose isTimeout()
will return true
:
1 2 3 4 5
foreach ($client->stream($responses, 1.5) as $response => $chunk) {
if ($chunk->isTimeout()) {
// $response stale for more than 1.5 seconds
}
}
A timeout is not necessarily an error: you can decide to stream again the response and get remaining contents that might come back in a new timeout, etc.
Tip
Passing 0
as timeout allows monitoring responses in a non-blocking way.
Note
Timeouts control how long one is willing to wait while the HTTP transaction is idle. Big responses can last as long as needed to complete, provided they remain active during the transfer and never pause for longer than specified.
Use the max_duration
option to limit the time a full request/response can last.
Dealing with Network Errors
Network errors (broken pipe, failed DNS resolution, etc.) are thrown as instances of TransportExceptionInterface.
First of all, you don't have to deal with them: letting errors bubble to your generic exception-handling stack might be really fine in most use cases.
If you want to handle them, here is what you need to know:
To catch errors, you need to wrap calls to $client->request()
but also calls
to any methods of the returned responses. This is because responses are lazy, so
that network errors can happen when calling e.g. getStatusCode()
too:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
// ...
try {
// both lines can potentially throw
$response = $client->request(...);
$headers = $response->getHeaders();
// ...
} catch (TransportExceptionInterface $e) {
// ...
}
Note
Because $response->getInfo()
is non-blocking, it shouldn't throw by design.
When multiplexing responses, you can deal with errors for individual streams by
catching TransportExceptionInterface
in the foreach loop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
foreach ($client->stream($responses) as $response => $chunk) {
try {
if ($chunk->isTimeout()) {
// ... decide what to do when a timeout occurs
// if you want to stop a response that timed out, don't miss
// calling $response->cancel() or the destructor of the response
// will try to complete it one more time
} elseif ($chunk->isFirst()) {
// if you want to check the status code, you must do it when the
// first chunk arrived, using $response->getStatusCode();
// not doing so might trigger an HttpExceptionInterface
} elseif ($chunk->isLast()) {
// ... do something with $response
}
} catch (TransportExceptionInterface $e) {
// ...
}
}
Caching Requests and Responses
This component provides a CachingHttpClient decorator that allows caching responses and serving them from the local storage for next requests. The implementation leverages the HttpCache class under the hood so that the HttpKernel component needs to be installed in your application:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\HttpClient\CachingHttpClient;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpKernel\HttpCache\Store;
$store = new Store('/path/to/cache/storage/');
$client = HttpClient::create();
$client = new CachingHttpClient($client, $store);
// this won't hit the network if the resource is already in the cache
$response = $client->request('GET', 'https://example.com/cacheable-resource');
CachingHttpClient
accepts a third argument to set the options of the HttpCache
.
Interoperability
The component is interoperable with four different abstractions for HTTP clients: Symfony Contracts, PSR-18, HTTPlug v1/v2 and native PHP streams. If your application uses libraries that need any of them, the component is compatible with all of them. They also benefit from autowiring aliases when the framework bundle is used.
If you are writing or maintaining a library that makes HTTP requests, you can decouple it from any specific HTTP client implementations by coding against either Symfony Contracts (recommended), PSR-18 or HTTPlug v2.
Symfony Contracts
The interfaces found in the symfony/http-client-contracts
package define
the primary abstractions implemented by the component. Its entry point is the
HttpClientInterface. That's the
interface you need to code against when a client is needed:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Symfony\Contracts\HttpClient\HttpClientInterface;
class MyApiLayer
{
private $client;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
// [...]
}
All request options mentioned above (e.g. timeout management) are also defined in the wordings of the interface, so that any compliant implementations (like this component) is guaranteed to provide them. That's a major difference with the other abstractions, which provide none related to the transport itself.
Another major feature covered by the Symfony Contracts is async/multiplexing, as described in the previous sections.
PSR-18 and PSR-17
This component implements the PSR-18 (HTTP Client) specifications via the
Psr18Client class, which is an adapter
to turn a Symfony HttpClientInterface
into a PSR-18 ClientInterface
.
This class also implements the relevant methods of PSR-17 to ease creating
request objects.
To use it, you need the psr/http-client
package and a PSR-17 implementation:
1 2 3 4 5 6 7 8 9 10
# installs the PSR-18 ClientInterface
$ composer require psr/http-client
# installs an efficient implementation of response and stream factories
# with autowiring aliases provided by Symfony Flex
$ composer require nyholm/psr7
# alternatively, install the php-http/discovery package to auto-discover
# any already installed implementations from common vendors:
# composer require php-http/discovery
Now you can make HTTP requests with the PSR-18 client as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
use Psr\Http\Client\ClientInterface;
class Symfony
{
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
public function getAvailableVersions(): array
{
$request = $this->client->createRequest('GET', 'https://symfony.com/versions.json');
$response = $this->client->sendRequest($request);
return json_decode($response->getBody()->getContents(), true);
}
}
1 2 3 4 5 6 7 8
use Symfony\Component\HttpClient\Psr18Client;
$client = new Psr18Client();
$request = $client->createRequest('GET', 'https://symfony.com/versions.json');
$response = $client->sendRequest($request);
$content = json_decode($response->getBody()->getContents(), true);
4.4
The PSR-17 factory methods of Psr18Client
were introduced in Symfony 4.4.
HTTPlug
4.4
Support for HTTPlug was introduced in Symfony 4.4.
The HTTPlug v1 specification was published before PSR-18 and is superseded by
it. As such, you should not use it in newly written code. The component is still
interoperable with libraries that require it thanks to the
HttplugClient class. Similarly to
Psr18Client
implementing relevant parts of PSR-17, HttplugClient
also
implements the factory methods defined in the related php-http/message-factory
package.
1 2 3 4 5 6 7 8 9
# Let's suppose php-http/httplug is already required by the lib you want to use
# installs an efficient implementation of response and stream factories
# with autowiring aliases provided by Symfony Flex
$ composer require nyholm/psr7
# alternatively, install the php-http/discovery package to auto-discover
# any already installed implementations from common vendors:
# composer require php-http/discovery
Let's say you want to instantiate a class with the following constructor, that requires HTTPlug dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
class SomeSdk
{
public function __construct(
HttpClient $httpClient,
RequestFactory $requestFactory,
StreamFactory $streamFactory
)
// [...]
}
Because HttplugClient
implements the three interfaces, you can use it this way:
1 2 3 4
use Symfony\Component\HttpClient\HttplugClient;
$httpClient = new HttplugClient();
$apiClient = new SomeSdk($httpClient, $httpClient, $httpClient);
If you'd like to work with promises, HttplugClient
also implements the
HttpAsyncClient
interface. To use it, you need to install the
guzzlehttp/promises
package:
1
$ composer require guzzlehttp/promises
Then you're ready to go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpClient\HttplugClient;
$httpClient = new HttplugClient();
$request = $httpClient->createRequest('GET', 'https://my.api.com/');
$promise = $httpClient->sendAsyncRequest($request)
->then(
function (ResponseInterface $response) {
echo 'Got status '.$response->getStatusCode();
return $response;
},
function (\Throwable $exception) {
echo 'Error: '.$exception->getMessage();
throw $exception;
}
);
// after you're done with sending several requests,
// you must wait for them to complete concurrently
// wait for a specific promise to resolve while monitoring them all
$response = $promise->wait();
// wait maximum 1 second for pending promises to resolve
$httpClient->wait(1.0);
// wait for all remaining promises to resolve
$httpClient->wait();
Native PHP Streams
4.4
Support for native PHP streams was introduced in Symfony 4.4.
Responses implementing ResponseInterface can be cast to native PHP streams with createResource(). This allows using them where native PHP streams are needed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\StreamWrapper;
$client = HttpClient::create();
$response = $client->request('GET', 'https://symfony.com/versions.json');
$streamResource = StreamWrapper::createResource($response, $client);
// alternatively and contrary to the previous one, this returns
// a resource that is seekable and potentially stream_select()-able
$streamResource = $response->toStream();
echo stream_get_contents($streamResource); // outputs the content of the response
// later on if you need to, you can access the response from the stream
$response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse();
Testing
This component includes the MockHttpClient
and MockResponse
classes to
use in tests that shouldn't make actual HTTP requests. Such tests can be
useful, as they will run faster and produce consistent results, since they're
not dependent on an external service. By not making actual HTTP requests there
is no need to worry about the service being online or the request changing
state, for example deleting a resource.
MockHttpClient
implements the HttpClientInterface
, just like any actual
HTTP client in this component. When you type-hint with HttpClientInterface
your code will accept the real client outside tests, while replacing it with
MockHttpClient
in the test.
When the request
method is used on MockHttpClient
, it will respond with
the supplied MockResponse
. There are a few ways to use it, as described
below.
HTTP Client and Responses
The first way of using MockHttpClient
is to pass a list of responses to its
constructor. These will be yielded in order when requests are made:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$responses = [
new MockResponse($body1, $info1),
new MockResponse($body2, $info2),
];
$client = new MockHttpClient($responses);
// responses are returned in the same order as passed to MockHttpClient
$response1 = $client->request('...'); // returns $responses[0]
$response2 = $client->request('...'); // returns $responses[1]
Another way of using MockHttpClient
is to pass a callback that generates the
responses dynamically when it's called:
1 2 3 4 5 6 7 8 9
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$callback = function ($method, $url, $options) {
return new MockResponse('...');
};
$client = new MockHttpClient($callback);
$response = $client->request('...'); // calls $callback to get the response
If you need to test responses with HTTP status codes different than 200,
define the http_code
option:
1 2 3 4 5 6 7 8 9
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$client = new MockHttpClient([
new MockResponse('...', ['http_code' => 500]),
new MockResponse('...', ['http_code' => 404]),
]);
$response = $client->request('...');
The responses provided to the mock client don't have to be instances of
MockResponse
. Any class implementing ResponseInterface
will work (e.g.
$this->createMock(ResponseInterface::class)
).
However, using MockResponse
allows simulating chunked responses and timeouts:
1 2 3 4 5 6 7 8
$body = function () {
yield 'hello';
// empty strings are turned into timeouts so that they are easy to test
yield '';
yield 'world';
};
$mockResponse = new MockResponse($body());
Testing Request Data
The MockResponse
class comes with some helper methods to test the request:
getRequestOptions()
- returns an array containing other information about the request such as headers, query parameters, body content etc.
Usage example:
1 2 3 4 5 6 7 8 9 10 11 12
$mockResponse = new MockResponse('', ['http_code' => 204]);
$httpClient = new MockHttpClient($mockResponse, 'https://example.com');
$response = $httpClient->request('DELETE', 'api/article/1337', [
'headers' => [
'Accept: */*',
'Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l',
],
]);
$mockResponse->getRequestOptions()['headers'];
// returns ["Accept: */*", "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l"]
Full Example
The following standalone example demonstrates a way to use the HTTP client and test it in a real application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
// ExternalArticleService.php
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class ExternalArticleService
{
private HttpClientInterface $httpClient;
public function __construct(HttpClientInterface $httpClient)
{
$this->httpClient = $httpClient;
}
public function createArticle(array $requestData): array
{
$requestJson = json_encode($requestData, JSON_THROW_ON_ERROR);
$response = $this->httpClient->request('POST', 'api/article', [
'headers' => [
'Content-Type: application/json',
'Accept: application/json',
],
'body' => $requestJson,
]);
if (201 !== $response->getStatusCode()) {
throw new Exception('Response status code is different than expected.');
}
// ... other checks
$responseJson = $response->getContent();
$responseData = json_decode($responseJson, true, 512, JSON_THROW_ON_ERROR);
return $responseData;
}
}
// ExternalArticleServiceTest.php
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class ExternalArticleServiceTest extends TestCase
{
public function testSubmitData(): void
{
// Arrange
$requestData = ['title' => 'Testing with Symfony HTTP Client'];
$expectedRequestData = json_encode($requestData, JSON_THROW_ON_ERROR);
$expectedResponseData = ['id' => 12345];
$mockResponseJson = json_encode($expectedResponseData, JSON_THROW_ON_ERROR);
$mockResponse = new MockResponse($mockResponseJson, [
'http_code' => 201,
'response_headers' => ['Content-Type: application/json'],
]);
$httpClient = new MockHttpClient($mockResponse, 'https://example.com');
$service = new ExternalArticleService($httpClient);
// Act
$responseData = $service->createArticle($requestData);
// Assert
self::assertContains(
'Content-Type: application/json',
$mockResponse->getRequestOptions()['headers']
);
self::assertSame($expectedRequestData, $mockResponse->getRequestOptions()['body']);
self::assertSame($responseData, $expectedResponseData);
}
}