mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-23 16:22:10 +01:00
2502 lines
94 KiB
ReStructuredText
2502 lines
94 KiB
ReStructuredText
HTTP Client
|
|
===========
|
|
|
|
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:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ composer require symfony/http-client
|
|
|
|
Basic Usage
|
|
-----------
|
|
|
|
Use the :class:`Symfony\\Component\\HttpClient\\HttpClient` class to make
|
|
requests. In the Symfony framework, this class is available as the
|
|
``http_client`` service. This service will be :doc:`autowired </service_container/autowiring>`
|
|
automatically when type-hinting for :class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: php-symfony
|
|
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
class SymfonyDocs
|
|
{
|
|
public function __construct(
|
|
private HttpClientInterface $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;
|
|
}
|
|
}
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
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:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/packages/framework.yaml
|
|
framework:
|
|
http_client:
|
|
default_options:
|
|
max_redirects: 7
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
$framework->httpClient()
|
|
->defaultOptions()
|
|
->maxRedirects(7)
|
|
;
|
|
};
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
$client = HttpClient::create([
|
|
'max_redirects' => 7,
|
|
]);
|
|
|
|
You can also use the :method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::withOptions`
|
|
method to retrieve a new instance of the client with new default options::
|
|
|
|
$this->client = $client->withOptions([
|
|
'base_uri' => 'https://...',
|
|
'headers' => ['header-name' => 'header-value'],
|
|
'extra' => ['my-key' => 'my-value'],
|
|
]);
|
|
|
|
Alternatively, the :class:`Symfony\\Component\\HttpClient\\HttpOptions` class
|
|
brings most of the available options with type-hinted getters and setters::
|
|
|
|
$this->client = $client->withOptions(
|
|
(new HttpOptions())
|
|
->setBaseUri('https://...')
|
|
// replaces *all* headers at once, and deletes the headers you do not provide
|
|
->setHeaders(['header-name' => 'header-value'])
|
|
// set or replace a single header using setHeader()
|
|
->setHeader('another-header-name', 'another-header-value')
|
|
->toArray()
|
|
);
|
|
|
|
.. versionadded:: 7.1
|
|
|
|
The :method:`Symfony\\Component\\HttpClient\\HttpOptions::setHeader`
|
|
method was introduced in Symfony 7.1.
|
|
|
|
Some options are described in this guide:
|
|
|
|
* `Authentication`_
|
|
* `Query String Parameters`_
|
|
* `Headers`_
|
|
* `Redirects`_
|
|
* `Retry Failed Requests`_
|
|
* `HTTP Proxies`_
|
|
* `Using URI Templates`_
|
|
|
|
Check out the full :ref:`http_client config reference <reference-http-client>`
|
|
to learn about all the options.
|
|
|
|
The HTTP client also has a configuration option called
|
|
:ref:`max_host_connections <reference-http-client-max-host-connections>`.
|
|
This option cannot be overridden per request:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/packages/framework.yaml
|
|
framework:
|
|
http_client:
|
|
max_host_connections: 10
|
|
# ...
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
$framework->httpClient()
|
|
->maxHostConnections(10)
|
|
// ...
|
|
;
|
|
};
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
$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 :class:`Symfony\\Component\\HttpClient\\ScopingHttpClient`) to
|
|
autoconfigure the HTTP client based on the requested URL:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# 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)%'
|
|
# ...
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
// only requests matching scope will use these options
|
|
$framework->httpClient()->scopedClient('github.client')
|
|
->scope('https://api\.github\.com')
|
|
->header('Accept', 'application/vnd.github.v3+json')
|
|
->header('Authorization', 'token %env(GITHUB_API_TOKEN)%')
|
|
// ...
|
|
;
|
|
|
|
// using base_url, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs"))
|
|
// will default to these options
|
|
$framework->httpClient()->scopedClient('github.client')
|
|
->baseUri('https://api.github.com')
|
|
->header('Accept', 'application/vnd.github.v3+json')
|
|
->header('Authorization', 'token %env(GITHUB_API_TOKEN)%')
|
|
// ...
|
|
;
|
|
};
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
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.
|
|
|
|
.. note::
|
|
|
|
The options passed to the ``request()`` method are merged with the default
|
|
options defined in the scoped client. The options passed to ``request()``
|
|
take precedence and override or extend the default ones.
|
|
|
|
If you use scoped clients in the Symfony framework, you must use any of the
|
|
methods defined by Symfony to :ref:`choose a specific service <services-wire-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 :ref:`base_uri option docs <reference-http-client-base-uri>` 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::
|
|
|
|
$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',
|
|
],
|
|
]);
|
|
|
|
Symfony's HTTP client is asynchronous by default. When you call ``request()``,
|
|
the HTTP request starts immediately, but the method returns without waiting for
|
|
a response. Your code only blocks when you actually need the response data::
|
|
|
|
// the request starts, but execution continues without waiting
|
|
$response = $client->request('GET', 'http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso');
|
|
|
|
// this blocks until the response headers are received
|
|
$contentType = $response->getHeaders()['content-type'][0];
|
|
|
|
// this blocks until the full response body is received
|
|
$content = $response->getContent();
|
|
|
|
The HTTP client also supports :ref:`concurrent requests <http-client-concurrent-requests>`
|
|
to make multiple HTTP requests in parallel, and :ref:`streaming responses <http-client-streaming-responses>`
|
|
to process response data in chunks for fully 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):
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# 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'
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
$framework->httpClient()->scopedClient('example_api')
|
|
->baseUri('https://example.com/')
|
|
// HTTP Basic authentication
|
|
->authBasic('the-username:the-password')
|
|
|
|
// HTTP Bearer authentication (also called token authentication)
|
|
->authBearer('the-bearer-token')
|
|
|
|
// Microsoft NTLM authentication
|
|
->authNtlm('the-username:the-password')
|
|
;
|
|
};
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
$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',
|
|
]);
|
|
|
|
.. code-block:: php
|
|
|
|
$response = $client->request('GET', 'https://...', [
|
|
// use a different HTTP Basic authentication only for this request
|
|
'auth_basic' => ['the-username', 'the-password'],
|
|
|
|
// ...
|
|
]);
|
|
|
|
.. note::
|
|
|
|
Basic Authentication can also be set by including the credentials in the URL,
|
|
such as: ``http://the-username:the-password@example.com``
|
|
|
|
.. 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/.
|
|
|
|
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::
|
|
|
|
// 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:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/packages/framework.yaml
|
|
framework:
|
|
http_client:
|
|
default_options:
|
|
headers:
|
|
'User-Agent': 'My Fancy App'
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
$framework->httpClient()
|
|
->defaultOptions()
|
|
->header('User-Agent', 'My Fancy App')
|
|
;
|
|
};
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
// 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::
|
|
|
|
// 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::
|
|
|
|
$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::
|
|
|
|
$response = $client->request('POST', 'https://...', [
|
|
'json' => ['param1' => 'value1', '...'],
|
|
]);
|
|
|
|
$decodedPayload = $response->toArray();
|
|
|
|
To submit a form with file uploads, pass the file handle to the ``body`` option::
|
|
|
|
$fileHandle = fopen('/path/to/the/file', 'r');
|
|
$client->request('POST', 'https://...', ['body' => ['the_file' => $fileHandle]]);
|
|
|
|
By default, this code will populate the filename and content-type with the data
|
|
of the opened file, but you can configure both with the PHP streaming configuration::
|
|
|
|
stream_context_set_option($fileHandle, 'http', 'filename', 'the-name.txt');
|
|
stream_context_set_option($fileHandle, 'http', 'content_type', 'my/content-type');
|
|
|
|
.. tip::
|
|
|
|
When using multidimensional arrays the :class:`Symfony\\Component\\Mime\\Part\\Multipart\\FormDataPart`
|
|
class automatically appends ``[key]`` to the name of the field::
|
|
|
|
$formData = new FormDataPart([
|
|
'array_field' => [
|
|
'some value',
|
|
'other value',
|
|
],
|
|
]);
|
|
|
|
$formData->getParts(); // Returns two instances of TextPart
|
|
// with the names "array_field[0]" and "array_field[1]"
|
|
|
|
This behavior can be bypassed by using the following array structure::
|
|
|
|
$formData = new FormDataPart([
|
|
['array_field' => 'some value'],
|
|
['array_field' => 'other value'],
|
|
]);
|
|
|
|
$formData->getParts(); // Returns two instances of TextPart both
|
|
// with the name "array_field"
|
|
|
|
The ``Content-Type`` of each form's part is detected automatically. However,
|
|
you can override it by passing a ``DataPart``::
|
|
|
|
use Symfony\Component\Mime\Part\DataPart;
|
|
|
|
$formData = new FormDataPart([
|
|
['json_data' => new DataPart(json_encode($json), null, 'application/json')]
|
|
]);
|
|
|
|
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)::
|
|
|
|
$client->request('POST', 'https://...', [
|
|
// ...
|
|
'body' => $formData->bodyToString(),
|
|
'headers' => $formData->getPreparedHeaders()->toArray(),
|
|
]);
|
|
|
|
If you need to add a custom HTTP header to the upload, you can do::
|
|
|
|
$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 :ref:`send cookies with the BrowserKit component <component-browserkit-sending-cookies>`,
|
|
which integrates seamlessly with the HttpClient component, or manually setting
|
|
`the Cookie HTTP request header`_ as follows::
|
|
|
|
use Symfony\Component\HttpClient\HttpClient;
|
|
use Symfony\Component\HttpFoundation\Cookie;
|
|
|
|
$client = HttpClient::create([
|
|
'headers' => [
|
|
// set one cookie as a name=value pair
|
|
'Cookie' => 'flavor=chocolate',
|
|
|
|
// you can set multiple cookies at once separating them with a ;
|
|
'Cookie' => 'flavor=chocolate; size=medium',
|
|
|
|
// if needed, encode the cookie value to ensure that it contains valid characters
|
|
'Cookie' => sprintf("%s=%s", 'foo', rawurlencode('...')),
|
|
],
|
|
]);
|
|
|
|
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
|
|
:class:`Symfony\\Component\\HttpClient\\Exception\\RedirectionException`)::
|
|
|
|
$response = $client->request('GET', 'https://...', [
|
|
// 0 means to not follow any redirect
|
|
'max_redirects' => 0,
|
|
]);
|
|
|
|
Retry Failed Requests
|
|
~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Sometimes, requests fail because of network issues or temporary server errors.
|
|
Symfony's HttpClient allows retrying failed requests automatically using the
|
|
:ref:`retry_failed option <reference-http-client-retry-failed>`.
|
|
|
|
By default, failed requests are retried up to 3 times, with an exponential delay
|
|
between retries (first retry = 1 second; third retry: 4 seconds) and only for
|
|
the following HTTP status codes: ``423``, ``425``, ``429``, ``502`` and ``503``
|
|
when using any HTTP method and ``500``, ``504``, ``507`` and ``510`` when using
|
|
an HTTP `idempotent method`_. Use the ``max_retries`` setting to configure the
|
|
amount of times a request is retried.
|
|
|
|
Check out the full list of configurable :ref:`retry_failed options <reference-http-client-retry-failed>`
|
|
to learn how to tweak each of them to fit your application needs.
|
|
|
|
When using the HttpClient outside of a Symfony application, use the
|
|
:class:`Symfony\\Component\\HttpClient\\RetryableHttpClient` class to wrap your
|
|
original HTTP client::
|
|
|
|
use Symfony\Component\HttpClient\RetryableHttpClient;
|
|
|
|
$client = new RetryableHttpClient(HttpClient::create());
|
|
|
|
The :class:`Symfony\\Component\\HttpClient\\RetryableHttpClient` uses a
|
|
:class:`Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface` to
|
|
decide if the request should be retried, and to define the waiting time between
|
|
each retry.
|
|
|
|
Retry Over Several Base URIs
|
|
............................
|
|
|
|
The ``RetryableHttpClient`` can be configured to use multiple base URIs. This
|
|
feature provides increased flexibility and reliability for making HTTP
|
|
requests. Pass an array of base URIs as option ``base_uri`` when making a
|
|
request::
|
|
|
|
$response = $client->request('GET', 'some-page', [
|
|
'base_uri' => [
|
|
// first request will use this base URI
|
|
'https://example.com/a/',
|
|
// if first request fails, the following base URI will be used
|
|
'https://example.com/b/',
|
|
],
|
|
]);
|
|
|
|
When the number of retries is higher than the number of base URIs, the
|
|
last base URI will be used for the remaining retries.
|
|
|
|
If you want to shuffle the order of base URIs for each retry attempt, nest the
|
|
base URIs you want to shuffle in an additional array::
|
|
|
|
$response = $client->request('GET', 'some-page', [
|
|
'base_uri' => [
|
|
[
|
|
// a single random URI from this array will be used for the first request
|
|
'https://example.com/a/',
|
|
'https://example.com/b/',
|
|
],
|
|
// non-nested base URIs are used in order
|
|
'https://example.com/c/',
|
|
],
|
|
]);
|
|
|
|
This feature allows for a more randomized approach to handling retries,
|
|
reducing the likelihood of repeatedly hitting the same failed base URI.
|
|
|
|
By using a nested array for the base URI, you can use this feature
|
|
to distribute the load among many nodes in a cluster of servers.
|
|
|
|
You can also configure the array of base URIs using the ``withOptions()``
|
|
method::
|
|
|
|
$client = $client->withOptions(['base_uri' => [
|
|
'https://example.com/a/',
|
|
'https://example.com/b/',
|
|
]]);
|
|
|
|
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 the ``http://...`` URL of the proxy to get through
|
|
|
|
* ``no_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::
|
|
|
|
$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
|
|
:class:`Symfony\\Contracts\\HttpClient\\Exception\\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
|
|
:ref:`http_client config reference <reference-http-client>`), but this is not
|
|
recommended in production.
|
|
|
|
SSRF (Server-side request forgery) Handling
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
`SSRF`_ allows an attacker to induce the backend application to make HTTP
|
|
requests to an arbitrary domain. These attacks can also target the internal
|
|
hosts and IPs of the attacked server.
|
|
|
|
If you use an :class:`Symfony\\Component\\HttpClient\\HttpClient` together
|
|
with user-provided URIs, it is probably a good idea to decorate it with a
|
|
:class:`Symfony\\Component\\HttpClient\\NoPrivateNetworkHttpClient`. This will
|
|
ensure local networks are made inaccessible to the HTTP client::
|
|
|
|
use Symfony\Component\HttpClient\HttpClient;
|
|
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
|
|
|
|
$client = new NoPrivateNetworkHttpClient(HttpClient::create());
|
|
// nothing changes when requesting public networks
|
|
$client->request('GET', 'https://example.com/');
|
|
|
|
// however, all requests to private networks are now blocked by default
|
|
$client->request('GET', 'http://localhost/');
|
|
|
|
// the second optional argument defines the networks to block
|
|
// in this example, requests from 104.26.14.0 to 104.26.15.255 will result in an exception
|
|
// but all the other requests, including other internal networks, will be allowed
|
|
$client = new NoPrivateNetworkHttpClient(HttpClient::create(), ['104.26.14.0/23']);
|
|
|
|
Profiling
|
|
~~~~~~~~~
|
|
|
|
When you are using the :class:`Symfony\\Component\\HttpClient\\TraceableHttpClient`,
|
|
responses content will be kept in memory and may exhaust it.
|
|
|
|
You can disable this behavior by setting the ``extra.trace_content`` option to ``false``
|
|
in your requests::
|
|
|
|
$response = $client->request('GET', 'https://...', [
|
|
'extra' => ['trace_content' => false],
|
|
]);
|
|
|
|
This setting won't affect other clients.
|
|
|
|
Using URI Templates
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
The :class:`Symfony\\Component\\HttpClient\\UriTemplateHttpClient` provides
|
|
a client that eases the use of URI templates, as described in the `RFC 6570`_::
|
|
|
|
$client = new UriTemplateHttpClient();
|
|
|
|
// this will make a request to the URL http://example.org/users?page=1
|
|
$client->request('GET', 'http://example.org/{resource}{?page}', [
|
|
'vars' => [
|
|
'resource' => 'users',
|
|
'page' => 1,
|
|
],
|
|
]);
|
|
|
|
Before using URI templates in your applications, you must install a third-party
|
|
package that expands those URI templates to turn them into URLs:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ composer require league/uri
|
|
|
|
# Symfony also supports the following URI template packages:
|
|
# composer require guzzlehttp/uri-template
|
|
# composer require rize/uri-template
|
|
|
|
When using this client in the framework context, all existing HTTP clients
|
|
are decorated by the :class:`Symfony\\Component\\HttpClient\\UriTemplateHttpClient`.
|
|
This means that URI template feature is enabled by default for all HTTP clients
|
|
you may use in your application.
|
|
|
|
You can configure variables that will be replaced globally in all URI templates
|
|
of your application:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/packages/framework.yaml
|
|
framework:
|
|
http_client:
|
|
default_options:
|
|
vars:
|
|
- secret: 'secret-token'
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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:vars name="secret">secret-token</framework:vars>
|
|
</framework:default-options>
|
|
</framework:http-client>
|
|
</framework:config>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework) {
|
|
$framework->httpClient()
|
|
->defaultOptions()
|
|
->vars(['secret' => 'secret-token'])
|
|
;
|
|
};
|
|
|
|
If you want to define your own logic to handle variables of URI templates, you
|
|
can do so by redefining the ``http_client.uri_template_expander`` alias. Your
|
|
service must be invokable.
|
|
|
|
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 can make HTTP requests using native PHP streams and the
|
|
``amphp/http-client`` and cURL libraries. Although they are interchangeable and
|
|
provide the same features, including concurrent requests, HTTP/2 is only supported
|
|
when using cURL or ``amphp/http-client``.
|
|
|
|
.. note::
|
|
|
|
To use the :class:`Symfony\\Component\\HttpClient\\AmpHttpClient`, the
|
|
`amphp/http-client`_ package must be installed.
|
|
|
|
The :method:`Symfony\\Component\\HttpClient\\HttpClient::create` method
|
|
selects the cURL transport if the `cURL PHP extension`_ is enabled. It falls
|
|
back to ``AmpHttpClient`` if cURL couldn't be found or is too old. Finally, if
|
|
``AmpHttpClient`` is not available, it falls back to PHP streams.
|
|
If you prefer to select the transport explicitly, use the following classes
|
|
to create the client::
|
|
|
|
use Symfony\Component\HttpClient\AmpHttpClient;
|
|
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();
|
|
|
|
// uses the client from the `amphp/http-client` package
|
|
$client = new AmpHttpClient();
|
|
|
|
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, and will fall back as explained above.
|
|
|
|
Configuring CurlHttpClient Options
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
PHP allows configuring lots of `cURL options`_ via the :phpfunction:`curl_setopt`
|
|
function. In order to make the component more portable when not using cURL, the
|
|
:class:`Symfony\\Component\\HttpClient\\CurlHttpClient` only uses some of those
|
|
options (and they are ignored in the rest of clients).
|
|
|
|
Add an ``extra.curl`` option in your configuration to pass those extra options::
|
|
|
|
use Symfony\Component\HttpClient\CurlHttpClient;
|
|
|
|
$client = new CurlHttpClient();
|
|
|
|
$client->request('POST', 'https://...', [
|
|
// ...
|
|
'extra' => [
|
|
'curl' => [
|
|
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V6,
|
|
],
|
|
],
|
|
]);
|
|
|
|
.. note::
|
|
|
|
Some cURL options are impossible to override (e.g. because of thread safety)
|
|
and you'll get an exception when trying to override them.
|
|
|
|
HTTP Compression
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
The HTTP header ``Accept-Encoding: gzip`` is added automatically if:
|
|
|
|
* using cURL client: cURL was compiled with ZLib support (see ``php --ri curl``)
|
|
* 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 support it.
|
|
|
|
.. warning::
|
|
|
|
If you set ``Accept-Encoding`` to e.g. ``gzip``, you will need to handle the
|
|
decompression yourself.
|
|
|
|
HTTP/2 Support
|
|
~~~~~~~~~~~~~~
|
|
|
|
When requesting an ``https`` URL, HTTP/2 is enabled by default if one of the
|
|
following tools is installed:
|
|
|
|
* The `libcurl`_ package version 7.36 or higher, used with PHP >= 7.2.17 / 7.3.4;
|
|
* The `amphp/http-client`_ Packagist package version 4.2 or higher.
|
|
|
|
To force HTTP/2 for ``http`` URLs, you need to enable it explicitly via the
|
|
``http_version`` option:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/packages/framework.yaml
|
|
framework:
|
|
http_client:
|
|
default_options:
|
|
http_version: '2.0'
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
$framework->httpClient()
|
|
->defaultOptions()
|
|
->httpVersion('2.0')
|
|
;
|
|
};
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
$client = HttpClient::create(['http_version' => '2.0']);
|
|
|
|
Support for HTTP/2 PUSH works automatically when using a compatible client:
|
|
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
|
|
:class:`Symfony\\Contracts\\HttpClient\\ResponseInterface` which provides the
|
|
following methods::
|
|
|
|
$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');
|
|
|
|
// the special "pause_handler" info item is a callable that allows you to delay the request
|
|
// for a given number of seconds; this allows you to delay retries, throttle streams, etc.
|
|
$response->getInfo('pause_handler')(2);
|
|
|
|
.. note::
|
|
|
|
``$response->toStream()`` is part of :class:`Symfony\\Component\\HttpClient\\Response\\StreamableInterface`.
|
|
|
|
.. 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.
|
|
|
|
.. _http-client-streaming-responses:
|
|
|
|
Streaming Responses
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
Call the :method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::stream`
|
|
method to get *chunks* of the response sequentially instead of waiting for the
|
|
entire response::
|
|
|
|
$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
|
|
:method:`Symfony\\Contracts\\HttpClient\\ResponseInterface::cancel`::
|
|
|
|
$response->cancel();
|
|
|
|
Or throw an exception from a progress callback::
|
|
|
|
$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
|
|
:class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface`
|
|
and will abort the request.
|
|
|
|
In case the response was canceled using ``$response->cancel()``,
|
|
``$response->getInfo('canceled')`` will return ``true``.
|
|
|
|
Handling Exceptions
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
There are three types of exceptions, all of which implement the
|
|
:class:`Symfony\\Contracts\\HttpClient\\Exception\\ExceptionInterface`:
|
|
|
|
* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\HttpExceptionInterface`
|
|
are thrown when your code does not handle the status codes in the 300-599 range.
|
|
|
|
* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface`
|
|
are thrown when a lower level issue occurs.
|
|
|
|
* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\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
|
|
:class:`Symfony\\Contracts\\HttpClient\\Exception\\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::
|
|
|
|
// 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::
|
|
|
|
$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.
|
|
|
|
.. _http-client-concurrent-requests:
|
|
|
|
Concurrent Requests
|
|
-------------------
|
|
|
|
Symfony's HTTP client makes asynchronous HTTP requests by default. This means
|
|
you don't need to configure anything special to send multiple requests in parallel
|
|
and process them efficiently.
|
|
|
|
Here's a practical example that fetches metadata about several Symfony
|
|
components from the Packagist API in parallel::
|
|
|
|
$packages = ['console', 'http-kernel', '...', 'routing', 'yaml'];
|
|
$responses = [];
|
|
foreach ($packages as $package) {
|
|
$uri = sprintf('https://repo.packagist.org/p2/symfony/%s.json', $package);
|
|
// send all requests concurrently (they won't block until response content is read)
|
|
$responses[$package] = $client->request('GET', $uri);
|
|
}
|
|
|
|
$results = [];
|
|
// iterate through the responses and read their content
|
|
foreach ($responses as $package => $response) {
|
|
// process response data somehow ...
|
|
$results[$package] = $response->toArray();
|
|
}
|
|
|
|
As you can see, the requests are sent in the first loop, but their responses
|
|
aren't consumed until the second one. This is the key to achieving parallel and
|
|
concurrent execution: dispatch all requests first, and read them later.
|
|
This allows the client to handle all pending responses efficiently while your
|
|
code waits only when necessary.
|
|
|
|
.. note::
|
|
|
|
The maximum number of concurrent requests depends on your system's resources
|
|
(e.g. the operating system might limit the number of simultaneous connections
|
|
or access to certificate files). To avoid hitting these limits, consider
|
|
processing requests in batches.
|
|
|
|
There is, however, a maximum amount of concurrent connections that can be open
|
|
per host (``6`` by default). See :ref:`max_host_connections <reference-http-client-max-host-connections>`.
|
|
|
|
Multiplexing Responses
|
|
~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
In the previous example, responses are read in the same order as the requests
|
|
were sent. However, it's possible that, for instance, the second response arrives
|
|
before the first. To handle such cases efficiently, you need fully asynchronous
|
|
processing, which allows responses to be handled in whatever order they arrive.
|
|
|
|
To achieve this, the
|
|
:method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::stream` method
|
|
can be used to monitor a list of responses. As mentioned
|
|
:ref:`previously <http-client-streaming-responses>`, this method yields response
|
|
chunks as soon as they arrive over the network. Replacing the standard ``foreach``
|
|
loop with the following version enables true asynchronous behavior::
|
|
|
|
foreach ($client->stream($responses) as $response => $chunk) {
|
|
if ($chunk->isFirst()) {
|
|
// the $response headers just arrived
|
|
// $response->getHeaders() is now non-blocking
|
|
} elseif ($chunk->isLast()) {
|
|
// the full $response body has been received
|
|
// $response->getContent() is now non-blocking
|
|
} else {
|
|
// $chunk->getContent() returns a piece of the body that just arrived
|
|
}
|
|
}
|
|
|
|
.. tip::
|
|
|
|
Use the ``user_data`` option along with ``$response->getInfo('user_data')``
|
|
to identify each response during streaming.
|
|
|
|
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::
|
|
|
|
// 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``::
|
|
|
|
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.
|
|
|
|
.. _http-client_network-errors:
|
|
|
|
Dealing with Network Errors
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Network errors (broken pipe, failed DNS resolution, etc.) are thrown as instances
|
|
of :class:`Symfony\\Contracts\\HttpClient\\Exception\\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::
|
|
|
|
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 :class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface`
|
|
in the foreach loop::
|
|
|
|
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 :class:`Symfony\\Component\\HttpClient\\CachingHttpClient`
|
|
decorator that allows caching responses and serving them from the local storage
|
|
for next requests. The implementation leverages the
|
|
:class:`Symfony\\Component\\HttpKernel\\HttpCache\\HttpCache` class internally
|
|
so that the :doc:`HttpKernel component </components/http_kernel>` needs to be
|
|
installed in your application::
|
|
|
|
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');
|
|
|
|
:class:`Symfony\\Component\\HttpClient\\CachingHttpClient` accepts a third argument
|
|
to set the options of the :class:`Symfony\\Component\\HttpKernel\\HttpCache\\HttpCache`.
|
|
|
|
Limit the Number of Requests
|
|
----------------------------
|
|
|
|
This component provides a :class:`Symfony\\Component\\HttpClient\\ThrottlingHttpClient`
|
|
decorator that allows you to limit the number of requests within a certain period,
|
|
potentially delaying calls based on the rate limiting policy.
|
|
|
|
The implementation leverages the
|
|
:class:`Symfony\\Component\\RateLimiter\\LimiterInterface` class under the hood
|
|
so the :doc:`Rate Limiter component </rate_limiter>` needs to be
|
|
installed in your application::
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/packages/framework.yaml
|
|
framework:
|
|
http_client:
|
|
scoped_clients:
|
|
example.client:
|
|
base_uri: 'https://example.com'
|
|
rate_limiter: 'http_example_limiter'
|
|
|
|
rate_limiter:
|
|
# Don't send more than 10 requests in 5 seconds
|
|
http_example_limiter:
|
|
policy: 'token_bucket'
|
|
limit: 10
|
|
rate: { interval: '5 seconds', amount: 10 }
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- 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:scoped-client name="example.client"
|
|
base-uri="https://example.com"
|
|
rate-limiter="http_example_limiter"
|
|
/>
|
|
</framework:http-client>
|
|
|
|
<framework:rate-limiter>
|
|
<!-- Don't send more than 10 requests in 5 seconds -->
|
|
<framework:limiter name="http_example_limiter"
|
|
policy="token_bucket"
|
|
limit="10"
|
|
>
|
|
<framework:rate interval="5 seconds" amount="10"/>
|
|
</framework:limiter>
|
|
</framework:rate-limiter>
|
|
</framework:config>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
$framework->httpClient()->scopedClient('example.client')
|
|
->baseUri('https://example.com')
|
|
->rateLimiter('http_example_limiter');
|
|
// ...
|
|
;
|
|
|
|
$framework->rateLimiter()
|
|
// Don't send more than 10 requests in 5 seconds
|
|
->limiter('http_example_limiter')
|
|
->policy('token_bucket')
|
|
->limit(10)
|
|
->rate()
|
|
->interval('5 seconds')
|
|
->amount(10)
|
|
;
|
|
};
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
use Symfony\Component\HttpClient\HttpClient;
|
|
use Symfony\Component\HttpClient\ThrottlingHttpClient;
|
|
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
|
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
|
|
|
|
$factory = new RateLimiterFactory([
|
|
'id' => 'http_example_limiter',
|
|
'policy' => 'token_bucket',
|
|
'limit' => 10,
|
|
'rate' => ['interval' => '5 seconds', 'amount' => 10],
|
|
], new InMemoryStorage());
|
|
$limiter = $factory->create();
|
|
|
|
$client = HttpClient::createForBaseUri('https://example.com');
|
|
$throttlingClient = new ThrottlingHttpClient($client, $limiter);
|
|
|
|
.. versionadded:: 7.1
|
|
|
|
The :class:`Symfony\\Component\\HttpClient\\ThrottlingHttpClient` was
|
|
introduced in Symfony 7.1.
|
|
|
|
Consuming Server-Sent Events
|
|
----------------------------
|
|
|
|
`Server-sent events`_ is an Internet standard used to push data to web pages.
|
|
Its JavaScript API is built around an `EventSource`_ object, which listens to
|
|
the events sent from some URL. The events are a stream of data (served with the
|
|
``text/event-stream`` MIME type) with the following format:
|
|
|
|
.. code-block:: text
|
|
|
|
data: This is the first message.
|
|
|
|
data: This is the second message, it
|
|
data: has two lines.
|
|
|
|
data: This is the third message.
|
|
|
|
Symfony's HTTP client provides an EventSource implementation to consume these
|
|
server-sent events. Use the :class:`Symfony\\Component\\HttpClient\\EventSourceHttpClient`
|
|
to wrap your HTTP client, open a connection to a server that responds with a
|
|
``text/event-stream`` content type and consume the stream as follows::
|
|
|
|
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
|
|
use Symfony\Component\HttpClient\EventSourceHttpClient;
|
|
|
|
// the second optional argument is the reconnection time in seconds (default = 10)
|
|
$client = new EventSourceHttpClient($client, 10);
|
|
$source = $client->connect('https://localhost:8080/events');
|
|
while ($source) {
|
|
foreach ($client->stream($source, 2) as $r => $chunk) {
|
|
if ($chunk->isTimeout()) {
|
|
// ...
|
|
continue;
|
|
}
|
|
|
|
if ($chunk->isLast()) {
|
|
// ...
|
|
|
|
return;
|
|
}
|
|
|
|
// this is a special ServerSentEvent chunk holding the pushed message
|
|
if ($chunk instanceof ServerSentEvent) {
|
|
// do something with the server event ...
|
|
}
|
|
}
|
|
}
|
|
|
|
.. tip::
|
|
|
|
If you know that the content of the ``ServerSentEvent`` is in the JSON format, you can
|
|
use the :method:`Symfony\\Component\\HttpClient\\Chunk\\ServerSentEvent::getArrayData`
|
|
method to directly get the decoded JSON as array.
|
|
|
|
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 :ref:`autowiring aliases <service-autowiring-alias>`
|
|
when the :doc:`framework bundle </reference/configuration/framework>` 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
|
|
:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`. That's the
|
|
interface you need to code against when a client is needed::
|
|
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
class MyApiLayer
|
|
{
|
|
public function __construct(
|
|
private HttpClientInterface $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
|
|
:class:`Symfony\\Component\\HttpClient\\Psr18Client` class, which is an adapter
|
|
to turn a Symfony :class:`Symfony\\Contracts\\HttpClient\\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:
|
|
|
|
.. code-block:: terminal
|
|
|
|
# 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:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: php-symfony
|
|
|
|
use Psr\Http\Client\ClientInterface;
|
|
|
|
class Symfony
|
|
{
|
|
public function __construct(
|
|
private ClientInterface $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);
|
|
}
|
|
}
|
|
|
|
.. code-block:: php-standalone
|
|
|
|
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);
|
|
|
|
You can also pass a set of default options to your client thanks to the
|
|
``Psr18Client::withOptions()`` method::
|
|
|
|
use Symfony\Component\HttpClient\Psr18Client;
|
|
|
|
$client = (new Psr18Client())
|
|
->withOptions([
|
|
'base_uri' => 'https://symfony.com',
|
|
'headers' => [
|
|
'Accept' => 'application/json',
|
|
],
|
|
]);
|
|
|
|
$request = $client->createRequest('GET', '/versions.json');
|
|
|
|
// ...
|
|
|
|
HTTPlug
|
|
~~~~~~~
|
|
|
|
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
|
|
:class:`Symfony\\Component\\HttpClient\\HttplugClient` class. Similarly to
|
|
:class:`Symfony\\Component\\HttpClient\\Psr18Client` implementing relevant parts of PSR-17,
|
|
:class:`Symfony\\Component\\HttpClient\\HttplugClient` also implements the factory methods
|
|
defined in the related ``php-http/message-factory`` package.
|
|
|
|
.. code-block:: terminal
|
|
|
|
# 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::
|
|
|
|
use Http\Client\HttpClient;
|
|
use Http\Message\StreamFactory;
|
|
|
|
class SomeSdk
|
|
{
|
|
public function __construct(
|
|
HttpClient $httpClient,
|
|
StreamFactory $streamFactory
|
|
)
|
|
// [...]
|
|
}
|
|
|
|
Because :class:`Symfony\\Component\\HttpClient\\HttplugClient` implements these
|
|
interfaces,you can use it this way::
|
|
|
|
use Symfony\Component\HttpClient\HttplugClient;
|
|
|
|
$httpClient = new HttplugClient();
|
|
$apiClient = new SomeSdk($httpClient, $httpClient);
|
|
|
|
If you'd like to work with promises, :class:`Symfony\\Component\\HttpClient\\HttplugClient`
|
|
also implements the ``HttpAsyncClient`` interface. To use it, you need to install the
|
|
``guzzlehttp/promises`` package:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ composer require guzzlehttp/promises
|
|
|
|
Then you're ready to go::
|
|
|
|
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): ResponseInterface {
|
|
echo 'Got status '.$response->getStatusCode();
|
|
|
|
return $response;
|
|
},
|
|
function (\Throwable $exception): never {
|
|
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();
|
|
|
|
You can also pass a set of default options to your client thanks to the
|
|
``HttplugClient::withOptions()`` method::
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Symfony\Component\HttpClient\HttplugClient;
|
|
|
|
$httpClient = (new HttplugClient())
|
|
->withOptions([
|
|
'base_uri' => 'https://my.api.com',
|
|
]);
|
|
$request = $httpClient->createRequest('GET', '/');
|
|
|
|
// ...
|
|
|
|
Native PHP Streams
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Responses implementing :class:`Symfony\\Contracts\\HttpClient\\ResponseInterface`
|
|
can be cast to native PHP streams with
|
|
:method:`Symfony\\Component\\HttpClient\\Response\\StreamWrapper::createResource`.
|
|
This allows using them where native PHP streams are needed::
|
|
|
|
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();
|
|
|
|
Extensibility
|
|
-------------
|
|
|
|
If you want to extend the behavior of a base HTTP client, you can use
|
|
:doc:`service decoration </service_container/service_decoration>`::
|
|
|
|
class MyExtendedHttpClient implements HttpClientInterface
|
|
{
|
|
public function __construct(
|
|
private ?HttpClientInterface $decoratedClient = null
|
|
) {
|
|
$this->decoratedClient ??= HttpClient::create();
|
|
}
|
|
|
|
public function request(string $method, string $url, array $options = []): ResponseInterface
|
|
{
|
|
// process and/or change the $method, $url and/or $options as needed
|
|
$response = $this->decoratedClient->request($method, $url, $options);
|
|
|
|
// if you call here any method on $response, the HTTP request
|
|
// won't be async; see below for a better way
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
|
{
|
|
return $this->decoratedClient->stream($responses, $timeout);
|
|
}
|
|
}
|
|
|
|
A decorator like this one is useful in cases where processing the requests'
|
|
arguments is enough. By decorating the ``on_progress`` option, you can
|
|
even implement basic monitoring of the response. However, since calling
|
|
responses' methods forces synchronous operations, doing so inside ``request()``
|
|
will break async.
|
|
|
|
The solution is to also decorate the response object itself.
|
|
:class:`Symfony\\Component\\HttpClient\\TraceableHttpClient` and
|
|
:class:`Symfony\\Component\\HttpClient\\Response\\TraceableResponse` are good
|
|
examples as a starting point.
|
|
|
|
In order to help writing more advanced response processors, the component provides
|
|
an :class:`Symfony\\Component\\HttpClient\\AsyncDecoratorTrait`. This trait allows
|
|
processing the stream of chunks as they come back from the network::
|
|
|
|
class MyExtendedHttpClient implements HttpClientInterface
|
|
{
|
|
use AsyncDecoratorTrait;
|
|
|
|
public function request(string $method, string $url, array $options = []): ResponseInterface
|
|
{
|
|
// process and/or change the $method, $url and/or $options as needed
|
|
|
|
$passthru = function (ChunkInterface $chunk, AsyncContext $context): \Generator {
|
|
// do what you want with chunks, e.g. split them
|
|
// in smaller chunks, group them, skip some, etc.
|
|
|
|
yield $chunk;
|
|
};
|
|
|
|
return new AsyncResponse($this->client, $method, $url, $options, $passthru);
|
|
}
|
|
}
|
|
|
|
Because the trait already implements a constructor and the ``stream()`` method,
|
|
you don't need to add them. The ``request()`` method should still be defined;
|
|
it shall return an
|
|
:class:`Symfony\\Component\\HttpClient\\Response\\AsyncResponse`.
|
|
|
|
The custom processing of chunks should happen in ``$passthru``: this generator
|
|
is where you need to write your logic. It will be called for each chunk yielded
|
|
by the underlying client. A ``$passthru`` that does nothing would just ``yield
|
|
$chunk;``. You could also yield a modified chunk, split the chunk into many
|
|
ones by yielding several times, or even skip a chunk altogether by issuing a
|
|
``return;`` instead of yielding.
|
|
|
|
In order to control the stream, the chunk passthru receives an
|
|
:class:`Symfony\\Component\\HttpClient\\Response\\AsyncContext` as second
|
|
argument. This context object has methods to read the current state of the
|
|
response. It also allows altering the response stream with methods to create
|
|
new chunks of content, pause the stream, cancel the stream, change the info of
|
|
the response, replace the current request by another one or change the chunk
|
|
passthru itself.
|
|
|
|
Checking the test cases implemented in
|
|
:class:`Symfony\\Component\\HttpClient\\Tests\\AsyncDecoratorTraitTest`
|
|
might be a good start to get various working examples for a better understanding.
|
|
Here are the use cases that it simulates:
|
|
|
|
* retry a failed request;
|
|
* send a preflight request, e.g. for authentication needs;
|
|
* issue subrequests and include their content in the main response's body.
|
|
|
|
The logic in :class:`Symfony\\Component\\HttpClient\\Response\\AsyncResponse`
|
|
has many safety checks that will throw a ``LogicException`` if the chunk
|
|
passthru doesn't behave correctly; e.g. if a chunk is yielded after an ``isLast()``
|
|
one, or if a content chunk is yielded before an ``isFirst()`` one, etc.
|
|
|
|
Testing
|
|
-------
|
|
|
|
This component includes the :class:`Symfony\\Component\\HttpClient\\MockHttpClient`
|
|
and :class:`Symfony\\Component\\HttpClient\\Response\\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.
|
|
|
|
:class:`Symfony\\Component\\HttpClient\\MockHttpClient` implements the
|
|
:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`, just like any actual
|
|
HTTP client in this component. When you type-hint with
|
|
:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface` your code will accept
|
|
the real client outside tests, while replacing it with
|
|
:class:`Symfony\\Component\\HttpClient\\MockHttpClient` in the test.
|
|
|
|
When the ``request`` method is used on :class:`Symfony\\Component\\HttpClient\\MockHttpClient`,
|
|
it will respond with the supplied
|
|
:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`. There are a few ways to use
|
|
it, as described below.
|
|
|
|
HTTP Client and Responses
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The first way of using :class:`Symfony\\Component\\HttpClient\\MockHttpClient`
|
|
is to pass a list of responses to its constructor. These will be yielded
|
|
in order when requests are made::
|
|
|
|
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]
|
|
|
|
It is also possible to create a
|
|
:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` directly
|
|
from a file, which is particularly useful when storing your response
|
|
snapshots in files::
|
|
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
|
|
$response = MockResponse::fromFile('tests/fixtures/response.xml');
|
|
|
|
.. versionadded:: 7.1
|
|
|
|
The :method:`Symfony\\Component\\HttpClient\\Response\\MockResponse::fromFile`
|
|
method was introduced in Symfony 7.1.
|
|
|
|
Another way of using :class:`Symfony\\Component\\HttpClient\\MockHttpClient` is to
|
|
pass a callback that generates the responses dynamically when it's called::
|
|
|
|
use Symfony\Component\HttpClient\MockHttpClient;
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
|
|
$callback = function ($method, $url, $options): MockResponse {
|
|
return new MockResponse('...');
|
|
};
|
|
|
|
$client = new MockHttpClient($callback);
|
|
$response = $client->request('...'); // calls $callback to get the response
|
|
|
|
You can also pass a list of callbacks if you need to perform specific
|
|
assertions on the request before returning the mocked response::
|
|
|
|
$expectedRequests = [
|
|
function ($method, $url, $options): MockResponse {
|
|
$this->assertSame('GET', $method);
|
|
$this->assertSame('https://example.com/api/v1/customer', $url);
|
|
|
|
return new MockResponse('...');
|
|
},
|
|
function ($method, $url, $options): MockResponse {
|
|
$this->assertSame('POST', $method);
|
|
$this->assertSame('https://example.com/api/v1/customer/1/products', $url);
|
|
|
|
return new MockResponse('...');
|
|
},
|
|
];
|
|
|
|
$client = new MockHttpClient($expectedRequests);
|
|
|
|
// ...
|
|
|
|
.. tip::
|
|
|
|
Instead of using the first argument, you can also set the (list of)
|
|
responses or callbacks using the
|
|
:method:`Symfony\\Component\\HttpClient\\MockHttpClient::setResponseFactory`
|
|
method::
|
|
|
|
$responses = [
|
|
new MockResponse($body1, $info1),
|
|
new MockResponse($body2, $info2),
|
|
];
|
|
|
|
$client = new MockHttpClient();
|
|
$client->setResponseFactory($responses);
|
|
|
|
If you need to test responses with HTTP status codes different than 200,
|
|
define the ``http_code`` option::
|
|
|
|
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
|
|
:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`. Any class
|
|
implementing :class:`Symfony\\Contracts\\HttpClient\\ResponseInterface`
|
|
will work (e.g. ``$this->createMock(ResponseInterface::class)``).
|
|
|
|
However, using :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`
|
|
allows simulating chunked responses and timeouts::
|
|
|
|
$body = function (): \Generator {
|
|
yield 'hello';
|
|
// empty strings are turned into timeouts so that they are easy to test
|
|
yield '';
|
|
yield 'world';
|
|
};
|
|
|
|
$mockResponse = new MockResponse($body());
|
|
|
|
Finally, you can also create an invokable or iterable class that generates the
|
|
responses and use it as a callback in functional tests::
|
|
|
|
namespace App\Tests;
|
|
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
|
|
|
class MockClientCallback
|
|
{
|
|
public function __invoke(string $method, string $url, array $options = []): ResponseInterface
|
|
{
|
|
// load a fixture file or generate data
|
|
// ...
|
|
return new MockResponse($data);
|
|
}
|
|
}
|
|
|
|
Then configure Symfony to use your callback:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/services_test.yaml
|
|
services:
|
|
# ...
|
|
App\Tests\MockClientCallback: ~
|
|
|
|
# config/packages/test/framework.yaml
|
|
framework:
|
|
http_client:
|
|
mock_response_factory: App\Tests\MockClientCallback
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- config/services_test.xml -->
|
|
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<container xmlns="http://symfony.com/schema/dic/services"
|
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsd:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
|
|
|
<services>
|
|
<service id="App\Tests\MockClientCallback"/>
|
|
</services>
|
|
</container>
|
|
|
|
<!-- 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 mock-response-factory="App\Tests\MockClientCallback">
|
|
<!-- ... -->
|
|
</framework-http-client>
|
|
</framework:config>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/framework.php
|
|
use Symfony\Config\FrameworkConfig;
|
|
|
|
return static function (FrameworkConfig $framework): void {
|
|
$framework->httpClient()
|
|
->mockResponseFactory(MockClientCallback::class)
|
|
;
|
|
};
|
|
|
|
To return json, you would normally do::
|
|
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
|
|
$response = new MockResponse(json_encode([
|
|
'foo' => 'bar',
|
|
]), [
|
|
'response_headers' => [
|
|
'content-type' => 'application/json',
|
|
],
|
|
]);
|
|
|
|
You can use :class:`Symfony\\Component\\HttpClient\\Response\\JsonMockResponse` instead::
|
|
|
|
use Symfony\Component\HttpClient\Response\JsonMockResponse;
|
|
|
|
$response = new JsonMockResponse([
|
|
'foo' => 'bar',
|
|
]);
|
|
|
|
Just like :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`, you can
|
|
also create a :class:`Symfony\\Component\\HttpClient\\Response\\JsonMockResponse`
|
|
directly from a file::
|
|
|
|
use Symfony\Component\HttpClient\Response\JsonMockResponse;
|
|
|
|
$response = JsonMockResponse::fromFile('tests/fixtures/response.json');
|
|
|
|
.. versionadded:: 7.1
|
|
|
|
The :method:`Symfony\\Component\\HttpClient\\Response\\JsonMockResponse::fromFile`
|
|
method was introduced in Symfony 7.1.
|
|
|
|
Testing Request Data
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` class comes
|
|
with some helper methods to test the request:
|
|
|
|
* ``getRequestMethod()`` - returns the HTTP method;
|
|
* ``getRequestUrl()`` - returns the URL the request would be sent to;
|
|
* ``getRequestOptions()`` - returns an array containing other information about
|
|
the request such as headers, query parameters, body content etc.
|
|
|
|
Usage example::
|
|
|
|
$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->getRequestMethod();
|
|
// returns "DELETE"
|
|
|
|
$mockResponse->getRequestUrl();
|
|
// returns "https://example.com/api/article/1337"
|
|
|
|
$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::
|
|
|
|
// ExternalArticleService.php
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
final class ExternalArticleService
|
|
{
|
|
public function __construct(
|
|
private HttpClientInterface $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
|
|
$this->assertSame('POST', $mockResponse->getRequestMethod());
|
|
$this->assertSame('https://example.com/api/article', $mockResponse->getRequestUrl());
|
|
$this->assertContains(
|
|
'Content-Type: application/json',
|
|
$mockResponse->getRequestOptions()['headers']
|
|
);
|
|
$this->assertSame($expectedRequestData, $mockResponse->getRequestOptions()['body']);
|
|
|
|
$this->assertSame($expectedResponseData, $responseData);
|
|
}
|
|
}
|
|
|
|
Testing Using HAR Files
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Modern browsers (via their network tab) and HTTP clients allow you to export the
|
|
information of one or more HTTP requests using the `HAR`_ (HTTP Archive) format.
|
|
You can use those ``.har`` files to perform tests with Symfony's HTTP Client.
|
|
|
|
First, use a browser or HTTP client to perform the HTTP request(s) you want to
|
|
test. Then, save that information as a ``.har`` file somewhere in your application::
|
|
|
|
// ExternalArticleServiceTest.php
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
use Symfony\Component\HttpClient\MockHttpClient;
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
|
|
final class ExternalArticleServiceTest extends KernelTestCase
|
|
{
|
|
public function testSubmitData(): void
|
|
{
|
|
// Arrange
|
|
$fixtureDir = sprintf('%s/tests/fixtures/HTTP', static::getContainer()->getParameter('kernel.project_dir'));
|
|
$factory = new HarFileResponseFactory("$fixtureDir/example.com_archive.har");
|
|
$httpClient = new MockHttpClient($factory, 'https://example.com');
|
|
$service = new ExternalArticleService($httpClient);
|
|
|
|
// Act
|
|
$responseData = $service->createArticle($requestData);
|
|
|
|
// Assert
|
|
$this->assertSame('the expected response', $responseData);
|
|
}
|
|
}
|
|
|
|
If your service performs multiple requests or if your ``.har`` file contains multiple
|
|
request/response pairs, the :class:`Symfony\\Component\\HttpClient\\Test\\HarFileResponseFactory`
|
|
will find the associated response based on the request method, URL and body (if any).
|
|
Note that **this won't work** if the request body or URI is random / always
|
|
changing (e.g. if it contains current date or random UUIDs).
|
|
|
|
Testing Network Transport Exceptions
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
As explained in the :ref:`Network Errors section <http-client_network-errors>`,
|
|
when making HTTP requests you might face errors at transport level.
|
|
|
|
That's why it's useful to test how your application behaves in case of a transport
|
|
error. :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` allows
|
|
you to do so in multiple ways.
|
|
|
|
In order to test errors that occur before headers have been received,
|
|
set the ``error`` option value when creating the ``MockResponse``.
|
|
Transport errors of this kind occur, for example, when a host name
|
|
cannot be resolved or the host was unreachable. The
|
|
``TransportException`` will be thrown as soon as a method like
|
|
``getStatusCode()`` or ``getHeaders()`` is called.
|
|
|
|
In order to test errors that occur while a response is being streamed
|
|
(that is, after the headers have already been received), provide the
|
|
exception to ``MockResponse`` as part of the ``body``
|
|
parameter. You can either use an exception directly, or yield the
|
|
exception from a callback. For exceptions of this kind,
|
|
``getStatusCode()`` may indicate a success (200), but accessing
|
|
``getContent()`` fails.
|
|
|
|
The following example code illustrates all three options.
|
|
|
|
body::
|
|
|
|
// ExternalArticleServiceTest.php
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\HttpClient\MockHttpClient;
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
|
|
final class ExternalArticleServiceTest extends TestCase
|
|
{
|
|
// ...
|
|
|
|
public function testTransportLevelError(): void
|
|
{
|
|
$requestData = ['title' => 'Testing with Symfony HTTP Client'];
|
|
$httpClient = new MockHttpClient([
|
|
// Mock a transport level error at a time before
|
|
// headers have been received (e. g. host unreachable)
|
|
new MockResponse(info: ['error' => 'host unreachable']),
|
|
|
|
// Mock a response with headers indicating
|
|
// success, but a failure while retrieving the body by
|
|
// creating the exception directly in the body...
|
|
new MockResponse([new \RuntimeException('Error at transport level')]),
|
|
|
|
// ... or by yielding it from a callback.
|
|
new MockResponse((static function (): \Generator {
|
|
yield new TransportException('Error at transport level');
|
|
})()),
|
|
]);
|
|
|
|
$service = new ExternalArticleService($httpClient);
|
|
|
|
try {
|
|
$service->createArticle($requestData);
|
|
|
|
// An exception should have been thrown in `createArticle()`, so this line should never be reached
|
|
$this->fail();
|
|
} catch (TransportException $e) {
|
|
$this->assertEquals(new \RuntimeException('Error at transport level'), $e->getPrevious());
|
|
$this->assertSame('Error at transport level', $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
.. _`cURL PHP extension`: https://www.php.net/curl
|
|
.. _`Zlib PHP extension`: https://www.php.net/zlib
|
|
.. _`PSR-17`: https://www.php-fig.org/psr/psr-17/
|
|
.. _`PSR-18`: https://www.php-fig.org/psr/psr-18/
|
|
.. _`HTTPlug`: https://github.com/php-http/httplug/#readme
|
|
.. _`Symfony Contracts`: https://github.com/symfony/contracts
|
|
.. _`libcurl`: https://curl.haxx.se/libcurl/
|
|
.. _`amphp/http-client`: https://packagist.org/packages/amphp/http-client
|
|
.. _`cURL options`: https://www.php.net/manual/en/function.curl-setopt.php
|
|
.. _`Server-sent events`: https://html.spec.whatwg.org/multipage/server-sent-events.html
|
|
.. _`EventSource`: https://www.w3.org/TR/eventsource/#eventsource
|
|
.. _`idempotent method`: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods
|
|
.. _`SSRF`: https://portswigger.net/web-security/ssrf
|
|
.. _`RFC 6570`: https://www.rfc-editor.org/rfc/rfc6570
|
|
.. _`HAR`: https://w3c.github.io/web-performance/specs/HAR/Overview.html
|
|
.. _`the Cookie HTTP request header`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie
|