diff --git a/components/http_client.rst b/components/http_client.rst new file mode 100644 index 00000000000..2a99653a1ed --- /dev/null +++ b/components/http_client.rst @@ -0,0 +1,537 @@ +.. index:: + single: HttpClient + single: Components; HttpClient + +The HttpClient Component +======================== + + The HttpClient component is a low-level HTTP client with support for both + PHP stream wrappers and cURL. It also provides utilities to consume APIs. + +.. versionadded:: 4.3 + + The HttpClient component was introduced in Symfony 4.3. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/http-client + +.. include:: /components/require_autoload.rst.inc + +Basic Usage +----------- + +Use the :class:`Symfony\\Component\\HttpClient\\HttpClient` class to create the +low-level HTTP client that makes requests, like the following ``GET`` request:: + + use Symfony\Component\HttpClient\HttpClient; + + $httpClient = HttpClient::create(); + $response = $httpClient->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', ...] + +Enabling cURL Support +--------------------- + +This component supports both the native PHP streams and cURL to make the HTTP +requests. Although both are interchangeable and provide the same features, +including concurrent requests, HTTP/2 is only supported when using cURL. + +``HttpClient::create()`` selects the cURL transport if the `cURL PHP extension`_ +is enabled and falls back to PHP streams otherwise. If you prefer to select +the transport explicitly, use the following classes to create the client:: + + use Symfony\Component\HttpClient\NativeHttpClient; + use Symfony\Component\HttpClient\CurlHttpClient; + + // uses native PHP streams + $httpClient = new NativeHttpClient(); + + // uses the cURL PHP extension + $httpClient = new CurlHttpClient(); + +When using this component in a full-stack Symfony application, this behavior is +not configurable and cURL will be used automatically if the cURL PHP extension +is installed and enabled. Otherwise, the native PHP streams will be used. + +Enabling HTTP/2 Support +----------------------- + +HTTP/2 is only supported when using the cURL-based transport and the libcurl +version is >= 7.36.0. If you meet these requirements, you can enable HTTP/2 +explicitly via the ``http_version`` option:: + + $httpClient = HttpClient::create(['http_version' => '2.0']); + +If you don't set the HTTP version explicitly, Symfony will use ``'2.0'`` only +when the request protocol is ``https://`` (and the cURL requirements mentioned +earlier are met). + +Making Requests +--------------- + +The client created with the ``HttpClient`` class provides a single ``request()`` +method to perform all kinds of HTTP requests:: + + $response = $httpClient->request('GET', 'https://...'); + $response = $httpClient->request('POST', 'https://...'); + $response = $httpClient->request('PUT', 'https://...'); + // ... + +Responses are always asynchronous, so they are ready as soon as the response +HTTP headers are received, instead of waiting to receive the entire response +contents:: + + $response = $httpClient->request('GET', 'http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso'); + + // code execution continues immediately; it doesn't wait to receive the response + // you can get the value of any HTTP response header + $contentType = $response->getHeaders()['content-type'][0]; + + // trying to get the response contents will block the execution until + // the full response contents are received + $contents = $response->getContent(); + +This component also supports :ref:`streaming responses ` +for full asynchronous applications. + +Authentication +~~~~~~~~~~~~~~ + +The HTTP client supports different authentication mechanisms. They can be +defined globally when creating the client (to apply it to all requests) and to +each request (which overrides any global authentication):: + + // Use the same authentication for all requests + $httpClient = HttpClient::create([ + // HTTP Basic authentication with only the username and not a password + 'auth_basic' => ['the-username'], + + // HTTP Basic authentication with a username and a password + 'auth_basic' => ['the-username', 'the-password'], + + // HTTP Bearer authentication (also called token authentication) + 'auth_bearer' => 'the-bearer-token', + ]); + + $response = $httpClient->request('GET', 'https://...', [ + // use a different HTTP Basic authentication only for this request + 'auth_basic' => ['the-username', 'the-password'], + + // ... + ]); + +Query String Parameters +~~~~~~~~~~~~~~~~~~~~~~~ + +You can either append them manually to the requested URL, or better, add them +as an associative array to the ``query`` option:: + + // it makes an HTTP GET request to https://httpbin.org/get?token=...&name=... + $response = $httpClient->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 both the default headers added to all +requests and the specific headers for each request:: + + // this header is added to all requests made by this client + $httpClient = HttpClient::create(['headers' => [ + 'Accept-Encoding' => 'gzip', + ]]); + + // this header is only included in this request and overrides the value + // of the same header if defined globally by the HTTP client + $response = $httpClient->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 and resources and they'll be +processed automatically when making the requests:: + + $response = $httpClient->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 () { + // ... + }, + + // 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 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 = $httpClient->request('POST', 'https://...', [ + 'json' => ['param1' => 'value1', '...'], + ]); + +Cookies +~~~~~~~ + +The HTTP client provided by this component is stateless but handling cookies +requires a stateful storage (because responses can update cookies and they must +be used for subsequent requests). That's why this component doesn't handle +cookies automatically. + +You can either handle cookies yourself using the ``Cookie`` HTTP header or use +the :doc:`BrowserKit component ` which provides this +feature and integrates seamlessly with the HttpClient component. + +Redirects +~~~~~~~~~ + +By default, the HTTP client follows redirects, up to a maximum of 20, when +making a request. Use the ``max_redirects`` setting to configure this behavior +(if the number of redirects is higher than the configured value, you'll get a +:class:`Symfony\\Component\\HttpClient\\Exception\\RedirectionException`):: + + $response = $httpClient->request('GET', 'https://...', [ + // 0 means to not follow any redirect + 'max_redirects' => 0, + ]); + +.. Concurrent Requests +.. ~~~~~~~~~~~~~~~~~~~ +.. +.. +.. TODO +.. +.. + +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 = $httpClient->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(); + + // 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'); + +.. _http-client-streaming-responses: + +Streaming Responses +~~~~~~~~~~~~~~~~~~~ + +Call to the ``stream()`` method of the HTTP client 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 = $httpClient->request('GET', $url, [ + // optional: if you don't want to buffer the response in memory + 'buffer' => false, + // optional: to display details about the response progress + 'on_progress' => function (int $dlNow, int $dlSize, array $info): void { + // ... + }, + ]); + + // Responses are lazy: this code is executed as soon as headers are received + if (200 !== $response->getStatusCode()) { + throw new \Exception('...'); + } + + // get the response contents in chunk and save them in a file + // response chunks implement Symfony\Contracts\HttpClient\ChunkInterface + $fileHandler = fopen('/ubuntu.iso', 'w'); + foreach ($httpClient->stream($response) as $chunk) { + fwrite($fileHandler, $chunk->getContent();); + } + +Handling Exceptions +~~~~~~~~~~~~~~~~~~~ + +When the HTTP status code of the response is not in the 200-299 range (i.e. 3xx, +4xx or 5xx) your code is expected to handle it. If you don't do that, the +``getHeaders()`` and ``getContent()`` methods throw an appropriate exception:: + + // the response of this request will be a 403 HTTP error + $response = $httpClient->request('GET', 'https://httpbin.org/status/403'); + + // this code results in a Symfony\Component\HttpClient\Exception\ClientException + // because it doesn't check the status code of the response + $content = $response->getContent(); + + // pass FALSE as the optional argument to not throw an exception and + // return instead an empty string + $content = $response->getContent(false); + +Caching Requests and Responses +------------------------------ + +This component provides a special HTTP client via the +:class:`Symfony\\Component\\HttpClient\\CachingHttpClient` class to cache +requests and their responses. The actual HTTP caching is implemented using the +:doc:`HttpKernel component `, so make sure it's +installed in your application. + +.. +.. TODO: +.. Show some example of caching requests+responses +.. +.. + +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, this component provides a special +HTTP client via the :class:`Symfony\\Component\\HttpClient\\ScopingHttpClient` +class to autoconfigure the HTTP client based on the requested URL:: + + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpClient\ScopingHttpClient; + + $client = HttpClient::create(); + $httpClient = new ScopingHttpClient($client, [ + // the key is a regexp which must match the beginning of the request URL + 'https://api\.github\.com/' => [ + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + 'Authorization' => 'token '.$githubToken, + ], + ], + + // use a '*' wildcard to apply some options to all requests + '*' => [ + // ... + ] + ]); + +If the request URL is relative (because you use the ``base_uri`` option), the +scoping HTTP client can't make a match. That's why you can define a third +optional argument in its constructor which will be considered the default +regular expression applied to relative URLs:: + + // ... + + $httpClient = new ScopingHttpClient($client, [ + 'https://api\.github\.com/' => [ + 'base_uri' => 'https://api.github.com/', + // ... + ], + + '*' => [ + // ... + ] + ], + // this is the regexp applied to all relative URLs + 'https://api\.github\.com/' + ); + +PSR-7 and PSR-18 Compatibility +------------------------------ + +This component uses its own interfaces and exception classes different from the +ones defined in `PSR-7`_ (HTTP message interfaces) and `PSR-18`_ (HTTP Client). +However, it includes the :class:`Symfony\\Component\\HttpClient\\Psr18Client` +class, which is an adapter to turn a Symfony ``HttpClientInterface`` into a +PSR-18 ``ClientInterface``. + +Before using it in your application, run the following commands to install the +required dependencies: + +.. code-block:: terminal + + # installs the base 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 + +Now you can make HTTP requests with the PSR-18 client as follows:: + + use Nyholm\Psr7\Factory\Psr17Factory; + use Symfony\Component\HttpClient\Psr18Client; + + $psr17Factory = new Psr17Factory(); + $psr18Client = new Psr18Client(); + + $url = 'https://symfony.com/versions.json'; + $request = $psr17Factory->createRequest('GET', $url); + $response = $psr18Client->sendRequest($request); + + $content = json_decode($response->getBody()->getContents(), true); + +Symfony Framework Integration +----------------------------- + +When using this component in a full-stack Symfony application, you can configure +multiple clients with different configurations and inject them into your services. + +Configuration +~~~~~~~~~~~~~ + +Use the ``framework.http_client`` key to configure the default HTTP client used +in the application. Check out the full +:ref:`http_client config reference ` to learn about all +the available config options: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + max_redirects: 7 + max_host_connections: 10 + +If you want to define multiple HTTP clients, use this other expanded configuration: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + http_clients: + crawler: + headers: [{ 'X-Powered-By': 'ACME App' }] + http_version: '1.0' + default: + max_host_connections: 10 + max_redirects: 7 + +Injecting the HTTP Client Into Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your application only defines one HTTP client, you can inject it into any +service by type-hinting a constructor argument with the +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`:: + + use Symfony\Contracts\HttpClient\HttpClientInterface; + + class SomeService + { + private $httpClient; + + public function __construct(HttpClientInterface $httpClient) + { + $this->httpClient = $httpClient; + } + } + +If you have several clients, you must use any of the methods defined by Symfony +to ref:`choose a specific service `. Each client +has a unique service named after its configuration. + +.. code-block:: yaml + + # config/services.yaml + services: + # ... + + # whenever a service type-hints HttpClientInterface, inject the GitHub client + Symfony\Contracts\HttpClient\HttpClientInterface: '@api_client.github' + + # inject the HTTP client called 'crawler' into this argument of this service + App\Some\Service: + $someArgument: '@http_client.crawler' + +Testing HTTP Clients and Responses +---------------------------------- + +This component includes the ``MockHttpClient`` and ``MockResponse`` classes to +use them in tests that need an HTTP client which doesn't make actual HTTP +requests. + +The first way of using ``MockHttpClient`` is to configure the set of responses +to return using its constructor:: + + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + $responses = [ + new MockResponse($body1, $info1), + new MockResponse($body2, $info2), + ]; + + $client = new MockHttpClient($responses); + // responses are returned in the same order as passed to MockHttpClient + $response1 = $client->request('...'); // returns $responses[0] + $response2 = $client->request('...'); // returns $responses[1] + +Another way of using ``MockHttpClient`` is to pass a callback that generates the +responses dynamically when it's called:: + + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + $callback = function ($method, $url, $options) { + return new MockResponse('...'); + }; + + $client = new MockHttpClient($callback); + $response = $client->request('...'); // calls $callback to get the response + +The responses provided to the mock client don't have to be instances of +``MockResponse``. Any class implementing ``ResponseInterface`` will work (e.g. +``$this->createMock(ResponseInterface::class)``). + +However, using ``MockResponse`` allows simulating chunked responses and timeouts:: + + $body = function () { + yield 'hello'; + // empty strings are turned into timeouts so that they are easy to test + yield ''; + yield 'world'; + }; + + $mockResponse = new MockResponse($body()); + +.. _`cURL PHP extension`: https://php.net/curl +.. _`PSR-7`: https://www.php-fig.org/psr/psr-7/ +.. _`PSR-18`: https://www.php-fig.org/psr/psr-18/ diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index 396df31d3d3..6c766f5f745 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -88,6 +88,33 @@ Configuration * `hinclude_default_template`_ * :ref:`path ` +* `http_client`_ + + * `auth_basic`_ + * `auth_bearer`_ + * `base_uri`_ + * `bindto`_ + * `buffer`_ + * `cafile`_ + * `capath`_ + * `capture_peer_cert_chain`_ + * `ciphers`_ + * `headers`_ + * `http_version`_ + * `local_cert`_ + * `local_pk`_ + * `max_host_connections`_ + * `max_redirects`_ + * `no_proxy`_ + * `passphrase`_ + * `peer_fingerprint`_ + * `proxy`_ + * `query`_ + * `resolve`_ + * `timeout`_ + * `verify_host`_ + * `verify_peer`_ + * `http_method_override`_ * `ide`_ * :ref:`lock ` @@ -626,6 +653,270 @@ path The path prefix for fragments. The fragment listener will only be executed when the request starts with this path. +.. _reference-http-client: + +http_client +~~~~~~~~~~~ + +If there's only one HTTP client defined in the app, you can configure it +directly under the ``framework.http_client`` option: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + headers: [{ 'X-Powered-By': 'ACME App' }] + max_host_connections: 10 + max_redirects: 7 + +If the app defines multiple HTTP clients, you must give them a unique name and +define them under the type of HTTP client you are creating (``http_clients`` for +regular clients and ``api_clients`` for clients that include utilities to +consume APIs): + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + http_clients: + crawler: + # ... + default: + # ... + api_clients: + github: + # ... + +auth_basic +.......... + +**type**: ``array`` + +The username and password used to create the ``Authorization`` HTTP header +used in HTTP Basic authentication. The value of this option must follow the +format ``['username', 'password']``. + +auth_bearer +........... + +**type**: ``string`` + +The token used to create the ``Authorization`` HTTP header used in HTTP Bearer +authentication (also called token authentication). + +base_uri +........ + +**type**: ``string`` + +URI that is merged into relative URIs, following the rules explained in the +`RFC 3986`_ standard. This is useful when all the requests you make share a +common prefix (e.g. ``https://api.github.com/``) so you can avoid adding it to +every request. + +Here are some common examples of how ``base_uri`` merging works in practice: + +=================== ============== ====================== +``base_uri`` Relative URI Actual Requested URI +=================== ============== ====================== +http://foo.com /bar http://foo.com/bar +http://foo.com/foo /bar http://foo.com/bar +http://foo.com/foo bar http://foo.com/bar +http://foo.com/foo/ bar http://foo.com/foo/bar +http://foo.com http://baz.com http://baz.com +http://foo.com/?bar bar http://foo.com/bar +=================== ============== ====================== + +bindto +...... + +**type**: ``string`` + +A network interface name, IP address, a host name or a UNIX socket to use as the +outgoing network interface. + +buffer +...... + +**type**: ``boolean`` + +.. TODO: improve this useless description + +Indicates if the response should be buffered or not. + +cafile +...... + +**type**: ``string`` + +The path of the certificate authority file that contains one or more +certificates used to verify the other servers' certificates. + +capath +...... + +**type**: ``string`` + +The path to a directory that contains one or more certificate authority files. + +capture_peer_cert_chain +....................... + +**type**: ``boolean`` + +If ``true``, the response includes a ``peer_certificate_chain`` attribute with +the peer certificates (OpenSSL X.509 resources). + +ciphers +....... + +**type**: ``string`` + +A list of the names of the ciphers allowed for the SSL/TLS connections. They +can be separated by colons, commas or spaces (e.g. ``'RC4-SHA:TLS13-AES-128-GCM-SHA256'``). + +headers +....... + +**type**: ``array`` + +An associative array of the HTTP headers added before making the request. This +value must use the format ``['header-name' => header-value, ...]``. + +http_version +............ + +**type**: ``string`` | ``null`` **default**: ``null`` + +The HTTP version to use, typically ``'1.1'`` or ``'2.0'``. Leave it to ``null`` +to let Symfony select the best version automatically. + +local_cert +.......... + +**type**: ``string`` + +The path to a file that contains the `PEM formatted`_ certificate used by the +HTTP client. This is often combined with the ``local_pk`` and ``passphrase`` +options. + +local_pk +........ + +**type**: ``string`` + +The path of a file that contains the `PEM formatted`_ private key of the +certificate defined in the ``local_cert`` option. + +max_host_connections +.................... + +**type**: ``integer`` **default**: ``6`` + +Defines the maximum amount of simultaneously open connections to a single host +(considering a "host" the same as a "host name + port number" pair). This limit +also applies for proxy connections, where the proxy is considered to be the host +for which this limit is applied. + +max_redirects +............. + +**type**: ``integer`` **default**: ``20`` + +The maximum number of redirects to follow. Use ``0`` to not follow any +redirection. + +no_proxy +........ + +**type**: ``string`` | ``null`` **default**: ``null`` + +A comma separated list of hosts that do not require a proxy to be reached, even +if one is configured. Use the ``'*'`` wildcard to match all hosts and an empty +string to match none (disables the proxy). + +passphrase +.......... + +**type**: ``string`` + +The passphrase used to encrypt the certificate stored in the file defined in the +``local_cert`` option. + +peer_fingerprint +................ + +**type**: ``array`` + +When negotiating a TLS or SSL connection, the server sends a certificate +indicating its identity. A public key is extracted from this certificate and if +it does not exactly match any of the public keys provided in this option, the +connection is aborted before sending or receiving any data. + +The value of this option is an associative array of ``algorithm => hash`` +(e.g ``['pin-sha256' => '...']``). + +proxy +..... + +**type**: ``string`` | ``null`` + +The HTTP proxy to use to make the requests. Leave it to ``null`` to detect the +proxy automatically based on your system configuration. + +query +..... + +**type**: ``array`` + +An associative array of the query string values added to the URL before making +the request. This value must use the format ``['parameter-name' => parameter-value, ...]``. + +resolve +....... + +**type**: ``array`` + +A list of hostnames and their IP addresses to pre-populate the DNS cache used by +the HTTP client in order to avoid a DNS lookup for those hosts. This option is +useful both to improve performance and to make your tests easier. + +The value of this option is an associative array of ``domain => IP address`` +(e.g ``['symfony.com' => '46.137.106.254', ...]``). + +timeout +....... + +**type**: ``float`` **default**: depends on your PHP config + +Time, in seconds, to wait for a response. If the response takes longer, a +:class:`Symfony\\Component\\HttpClient\\Exception\\TransportException` is thrown. +Its default value is the same as the value of PHP's `default_socket_timeout`_ +config option. + +verify_host +........... + +**type**: ``boolean`` + +If ``true``, the certificate sent by other servers is verified to ensure that +their common name matches the host included in the URL. This is usually +combined with ``verify_peer`` to also verify the certificate authenticity. + +verify_peer +........... + +**type**: ``boolean`` + +If ``true``, the certificate sent by other servers when negotiating a TLS or SSL +connection is verified for authenticity. Authenticating the certificate is not +enough to be sure about the server, so you should combine this with the +``verify_host`` option. + profiler ~~~~~~~~ @@ -2410,3 +2701,6 @@ to know their differences. .. _`session.sid_length PHP option`: https://php.net/manual/session.configuration.php#ini.session.sid-length .. _`session.sid_bits_per_character PHP option`: https://php.net/manual/session.configuration.php#ini.session.sid-bits-per-character .. _`X-Robots-Tag HTTP header`: https://developers.google.com/search/reference/robots_meta_tag +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`default_socket_timeout`: https://php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout +.. _`PEM formatted`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail