From bcb8a3c654171c30643a00d2697d07cbeac1c64f Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 1 Mar 2019 11:26:34 +0100 Subject: [PATCH 01/11] Documented the new HttpClient component --- components/http_client.rst | 355 ++++++++++++++++++++++++++ reference/configuration/framework.rst | 283 ++++++++++++++++++++ 2 files changed, 638 insertions(+) create mode 100644 components/http_client.rst diff --git a/components/http_client.rst b/components/http_client.rst new file mode 100644 index 00000000000..4e886dbd28f --- /dev/null +++ b/components/http_client.rst @@ -0,0 +1,355 @@ +.. 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 + +Alternatively, you can clone the ``_ +repository. + +.. 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",...}' + +If you are consuming APIs, you should use instead the +:class:`Symfony\\Component\\HttpClient\\ApiClient` class, which includes +shortcuts and utilities for common operations:: + + use Symfony\Component\HttpClient\ApiClient; + use Symfony\Component\HttpClient\HttpClient; + + $httpClient = HttpClient::create(); + $apiClient = new ApiClient($httpClient); + + $response = $apiClient->post('https://api.github.com/gists', [ + // this is transformed into the proper Basic authorization header + 'auth' => 'username:password', + // this PHP array is encoded as JSON and added to the request body + 'json' => [ + 'description' => 'Created by Symfony HttpClient', + 'public' => true, + 'files' => [ + 'article.txt' => ['content' => 'Lorem Ipsum ...'], + ], + ], + ]); + + // decodes the JSON body of the response into a PHP array + $result = $response->asArray(); + // $result = [ + // 'id' => '11b5f...023cf9', + // 'url' => 'https://api.github.com/gists/11b5f...023cf9', + // ... + // ] + +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 (and if +the installed cURL version supports it). + +``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. + +Making Requests +--------------- + +The client created with the ``HttpClient`` class provides a single ``request()`` +method to perform all HTTP requests, whereas the client created with the +``ApiClient`` class provides a method for each HTTP verb:: + + $response = $httpClient->request('GET', 'https://'); + $response = $httpClient->request('POST', 'https://'); + $response = $httpClient->request('PUT', 'https://'); + // ... + + $response = $apiClient->get('https://'); + $response = $apiClient->post('https://'); + $response = $apiClient->put('https://'); + // ... + +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' => [ + 'Authorization' => 'Bearer ...', + ]]); + + // this header is only included in this request + $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 +processing automatically when making the requests:: + + $response = $httpClient->request('POST', 'https://...', [ + // defining data using a regular string + 'body' => 'raw data', + + // 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 adds the required +``'Content-Type: application/x-www-form-urlencoded'`` header for you. + +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 "raw_headers", + // "redirect_count", "start_time", "redirect_url", etc. + $httpInfo = $response->getInfo(); + +Streaming Responses +~~~~~~~~~~~~~~~~~~~ + + +.. TODO: + + +Handling Exceptions +~~~~~~~~~~~~~~~~~~~ + +When an HTTP error happens (status code 3xx, 4xx or 5xx) your code is expected +to handle it by checking the status code of the response. If you don't do that, +an appropriate exception is thrown:: + + $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(); + + // this code doesn't throw an exception because it handles the HTTP error itself + $content = 403 !== $reponse->getStatusCode() ? $response->getContent() : null; + +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 app, 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 + +Symfony Framework Integration +----------------------------- + +When using this component in a full-stack Symfony application, you can configure +multiple clients with different configurations and inject them in your services. + +Configuration +~~~~~~~~~~~~~ + +Use the ``framework.http_client`` option to configure the default HTTP client +used in the application: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + max_redirects: 7 + max_host_connections: 10 + +If you want to define multiple HTTP and API 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 + + api_clients: + github: + base_uri: 'https://api.github.com' + headers: [{ 'Accept': 'application/vnd.github.v3+json' }] + +Injecting the HTTP Client Into Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your application only defines one HTTP client, you can inject it in 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 service associated with it whose name follows the pattern +``client type + client name`` (e.g. ``http_client.crawler``, ``api_client.github``). + +.. code-block:: yaml + + # config/services.yaml + services: + # ... + + # whenever a service type-hints ApiClientInterface, inject the GitHub client + Symfony\Contracts\HttpClient\ApiClientInterface: '@api_client.github' + + # inject the HTTP client called 'crawler' in this argument of this service + App\Some\Service: + $someArgument: '@http_client.crawler' + +.. _`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..bd4adad265d 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -88,6 +88,32 @@ Configuration * `hinclude_default_template`_ * :ref:`path ` +* `http_client`_ + + * `auth`_ + * `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 +652,260 @@ path The path prefix for fragments. The fragment listener will only be executed when the request starts with this path. +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 +.... + +**type**: ``string`` + +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'`` (a colon separates both values). + +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 +2690,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 From 943be07a68e037dd3376c01c884c3d07043edc25 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 4 Mar 2019 10:09:40 +0100 Subject: [PATCH 02/11] Fixes after the reviews --- components/http_client.rst | 37 +++++++++++++++++++++------ reference/configuration/framework.rst | 2 ++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index 4e886dbd28f..b829177020e 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -27,7 +27,7 @@ repository. Basic Usage ----------- -Use the :class:`Symfony\Component\HttpClient\HttpClient` class to create the +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; @@ -78,8 +78,7 @@ 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 (and if -the installed cURL version supports it). +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 @@ -98,6 +97,19 @@ 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 --------------- @@ -204,6 +216,13 @@ Concurrent Requests .. TODO +Asynchronous Requests +~~~~~~~~~~~~~~~~~~~~~ + + +.. TODO see https://gist.github.com/tgalopin/a84a11ece0621b8a79ed923afe015b3c + + Processing Responses -------------------- @@ -274,13 +293,15 @@ Symfony Framework Integration ----------------------------- When using this component in a full-stack Symfony application, you can configure -multiple clients with different configurations and inject them in your services. +multiple clients with different configurations and inject them into your services. Configuration ~~~~~~~~~~~~~ -Use the ``framework.http_client`` option to configure the default HTTP client -used in the application: +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 @@ -316,7 +337,7 @@ configuration: Injecting the HTTP Client Into Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If your application only defines one HTTP client, you can inject it in any +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`:: @@ -346,7 +367,7 @@ has a service associated with it whose name follows the pattern # whenever a service type-hints ApiClientInterface, inject the GitHub client Symfony\Contracts\HttpClient\ApiClientInterface: '@api_client.github' - # inject the HTTP client called 'crawler' in this argument of this service + # inject the HTTP client called 'crawler' into this argument of this service App\Some\Service: $someArgument: '@http_client.crawler' diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index bd4adad265d..acb65d731a2 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -652,6 +652,8 @@ 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 ~~~~~~~~~~~ From 744da20d201241bed5176b5ec21a1acddf102f53 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 8 Mar 2019 09:07:53 +0100 Subject: [PATCH 03/11] Some updates after the latest changes --- components/http_client.rst | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index b829177020e..5f77db32268 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -117,14 +117,14 @@ The client created with the ``HttpClient`` class provides a single ``request()`` method to perform all HTTP requests, whereas the client created with the ``ApiClient`` class provides a method for each HTTP verb:: - $response = $httpClient->request('GET', 'https://'); - $response = $httpClient->request('POST', 'https://'); - $response = $httpClient->request('PUT', 'https://'); + $response = $httpClient->request('GET', 'https://...'); + $response = $httpClient->request('POST', 'https://...'); + $response = $httpClient->request('PUT', 'https://...'); // ... - $response = $apiClient->get('https://'); - $response = $apiClient->post('https://'); - $response = $apiClient->put('https://'); + $response = $apiClient->get('https://...'); + $response = $apiClient->post('https://...'); + $response = $apiClient->put('https://...'); // ... Query String Parameters @@ -153,7 +153,8 @@ requests and the specific headers for each request:: 'Authorization' => 'Bearer ...', ]]); - // this header is only included in this request + // 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', @@ -165,7 +166,7 @@ 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 -processing automatically when making the requests:: +processed automatically when making the requests:: $response = $httpClient->request('POST', 'https://...', [ // defining data using a regular string @@ -244,6 +245,8 @@ following methods:: // returns info coming from the transport layer, such as "raw_headers", // "redirect_count", "start_time", "redirect_url", etc. $httpInfo = $response->getInfo(); + // you can get individual info too + $startTime = $response->getInfo('start_time'); Streaming Responses ~~~~~~~~~~~~~~~~~~~ @@ -257,7 +260,7 @@ Handling Exceptions When an HTTP error happens (status code 3xx, 4xx or 5xx) your code is expected to handle it by checking the status code of the response. If you don't do that, -an appropriate exception is thrown:: +the ``getHeaders()`` and ``getContent()`` methods throw an appropriate exception:: $response = $httpClient->request('GET', 'https://httpbin.org/status/403'); @@ -265,8 +268,9 @@ an appropriate exception is thrown:: // because it doesn't check the status code of the response $content = $response->getContent(); - // this code doesn't throw an exception because it handles the HTTP error itself - $content = 403 !== $reponse->getStatusCode() ? $response->getContent() : null; + // pass FALSE as the optional argument to not throw an exception and + // return instead an empty string + $content = $response->getContent(false); PSR-7 and PSR-18 Compatibility ------------------------------ From c8375fd619dbc67f8e77268b385ebc66d4f5923e Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Thu, 14 Mar 2019 09:42:02 +0100 Subject: [PATCH 04/11] Updates about authentication --- components/http_client.rst | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index 5f77db32268..78530951d2a 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -54,7 +54,7 @@ shortcuts and utilities for common operations:: $response = $apiClient->post('https://api.github.com/gists', [ // this is transformed into the proper Basic authorization header - 'auth' => 'username:password', + 'auth_basic' => ['username', 'password'], // this PHP array is encoded as JSON and added to the request body 'json' => [ 'description' => 'Created by Symfony HttpClient', @@ -127,6 +127,32 @@ method to perform all HTTP requests, whereas the client created with the $response = $apiClient->put('https://...'); // ... +Authentication +~~~~~~~~~~~~~~ + +The HTTP and API clients support 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, if defined):: + + // 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 ~~~~~~~~~~~~~~~~~~~~~~~ @@ -150,7 +176,7 @@ requests and the specific headers for each request:: // this header is added to all requests made by this client $httpClient = HttpClient::create(['headers' => [ - 'Authorization' => 'Bearer ...', + 'Accept-Encoding' => 'gzip', ]]); // this header is only included in this request and overrides the value From 47cec6bd5830530edeef0babf66c8dc58a6ee9a0 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 20 Mar 2019 08:47:06 +0100 Subject: [PATCH 05/11] Added a section about testing --- components/http_client.rst | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/components/http_client.rst b/components/http_client.rst index 78530951d2a..46054271b8c 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -401,6 +401,57 @@ has a service associated with it whose name follows the pattern 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->getMockBuilder(ResponseInterface::class)->getMock()``). + +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/ From 75c9dbf20294fe866497db62a020bd96d133b2db Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 29 Mar 2019 16:43:04 +0100 Subject: [PATCH 06/11] Updates and fixes --- components/http_client.rst | 101 ++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index 46054271b8c..7d17e4a55f9 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -40,38 +40,9 @@ low-level HTTP client that makes requests, like the following ``GET`` request:: $contentType = $response->getHeaders()['content-type'][0]; // $contentType = 'application/json' $content = $response->getContent(); - // $content = '{"id":521583,"name":"symfony-docs",...}' - -If you are consuming APIs, you should use instead the -:class:`Symfony\\Component\\HttpClient\\ApiClient` class, which includes -shortcuts and utilities for common operations:: - - use Symfony\Component\HttpClient\ApiClient; - use Symfony\Component\HttpClient\HttpClient; - - $httpClient = HttpClient::create(); - $apiClient = new ApiClient($httpClient); - - $response = $apiClient->post('https://api.github.com/gists', [ - // this is transformed into the proper Basic authorization header - 'auth_basic' => ['username', 'password'], - // this PHP array is encoded as JSON and added to the request body - 'json' => [ - 'description' => 'Created by Symfony HttpClient', - 'public' => true, - 'files' => [ - 'article.txt' => ['content' => 'Lorem Ipsum ...'], - ], - ], - ]); - - // decodes the JSON body of the response into a PHP array - $result = $response->asArray(); - // $result = [ - // 'id' => '11b5f...023cf9', - // 'url' => 'https://api.github.com/gists/11b5f...023cf9', - // ... - // ] + // $content = '{"id":521583, "name":"symfony-docs", ...}' + $content = $response->toArray(); + // $content = ['id' => 521583, 'name' => 'symfony-docs', ...] Enabling cURL Support --------------------- @@ -114,25 +85,19 @@ Making Requests --------------- The client created with the ``HttpClient`` class provides a single ``request()`` -method to perform all HTTP requests, whereas the client created with the -``ApiClient`` class provides a method for each HTTP verb:: +method to perform all kinds of HTTP requests:: $response = $httpClient->request('GET', 'https://...'); $response = $httpClient->request('POST', 'https://...'); $response = $httpClient->request('PUT', 'https://...'); // ... - $response = $apiClient->get('https://...'); - $response = $apiClient->post('https://...'); - $response = $apiClient->put('https://...'); - // ... - Authentication ~~~~~~~~~~~~~~ -The HTTP and API clients support 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, if defined):: +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, if defined):: // Use the same authentication for all requests $httpClient = HttpClient::create([ @@ -198,6 +163,9 @@ processed automatically when making the requests:: // 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 () { // ... @@ -211,6 +179,14 @@ When uploading data with the ``POST`` method, if you don't define the ``Content-Type`` HTTP header explicitly, Symfony 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 ~~~~~~~ @@ -277,9 +253,30 @@ following methods:: 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:: -.. TODO: + $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) { + // ... + }, + ]); + // 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 ~~~~~~~~~~~~~~~~~~~ @@ -307,8 +304,8 @@ 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 app, run the following commands to install the required -dependencies: +Before using it in your application, run the following commands to install the +required dependencies: .. code-block:: terminal @@ -319,6 +316,20 @@ dependencies: # 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 ----------------------------- From 49ab6e9e8266ba4551834e6936aa7ff95423ca5c Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 29 Mar 2019 17:16:02 +0100 Subject: [PATCH 07/11] Added docs for Caching and Scoping clients --- components/http_client.rst | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/components/http_client.rst b/components/http_client.rst index 7d17e4a55f9..19bf97eb250 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -295,6 +295,68 @@ the ``getHeaders()`` and ``getContent()`` methods throw an appropriate exception // 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: check the PHPdoc of the class: +.. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpClient/CachingHttpClient.php +.. 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 ------------------------------ From 126ba21449a7a6212bdd6965a8f6aaed15baa2f5 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 29 Apr 2019 09:49:45 +0200 Subject: [PATCH 08/11] Final changes --- components/http_client.rst | 48 ++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index 19bf97eb250..c8ac51370a2 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -19,9 +19,6 @@ Installation $ composer require symfony/http-client -Alternatively, you can clone the ``_ -repository. - .. include:: /components/require_autoload.rst.inc Basic Usage @@ -92,12 +89,29 @@ method to perform all kinds of HTTP requests:: $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, if defined):: +each request (which overrides any global authentication):: // Use the same authentication for all requests $httpClient = HttpClient::create([ @@ -219,13 +233,6 @@ Concurrent Requests .. TODO -Asynchronous Requests -~~~~~~~~~~~~~~~~~~~~~ - - -.. TODO see https://gist.github.com/tgalopin/a84a11ece0621b8a79ed923afe015b3c - - Processing Responses -------------------- @@ -244,12 +251,14 @@ following methods:: // gets the response body as a string $content = $response->getContent(); - // returns info coming from the transport layer, such as "raw_headers", + // 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 ~~~~~~~~~~~~~~~~~~~ @@ -415,8 +424,7 @@ the available config options: max_redirects: 7 max_host_connections: 10 -If you want to define multiple HTTP and API clients, use this other expanded -configuration: +If you want to define multiple HTTP clients, use this other expanded configuration: .. code-block:: yaml @@ -432,11 +440,6 @@ configuration: max_host_connections: 10 max_redirects: 7 - api_clients: - github: - base_uri: 'https://api.github.com' - headers: [{ 'Accept': 'application/vnd.github.v3+json' }] - Injecting the HTTP Client Into Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -458,8 +461,7 @@ service by type-hinting a constructor argument with the 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 service associated with it whose name follows the pattern -``client type + client name`` (e.g. ``http_client.crawler``, ``api_client.github``). +has a unique service named after its configuration. .. code-block:: yaml @@ -467,8 +469,8 @@ has a service associated with it whose name follows the pattern services: # ... - # whenever a service type-hints ApiClientInterface, inject the GitHub client - Symfony\Contracts\HttpClient\ApiClientInterface: '@api_client.github' + # 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: From a1fe401c2ad7dcddeaea5f63113136a389b83284 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 29 Apr 2019 10:00:41 +0200 Subject: [PATCH 09/11] Final fixes --- components/http_client.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index c8ac51370a2..419053e1e77 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -162,7 +162,7 @@ requests and the specific headers for each request:: // of the same header if defined globally by the HTTP client $response = $httpClient->request('POST', 'https://...', [ 'headers' => [ - 'Content-type' => 'text/plain', + 'Content-Type' => 'text/plain', ], ]); @@ -190,7 +190,8 @@ processed automatically when making the requests:: ]); When uploading data with the ``POST`` method, if you don't define the -``Content-Type`` HTTP header explicitly, Symfony adds the required +``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 @@ -270,7 +271,7 @@ response sequentially instead of waiting for the entire response:: // 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) { + 'on_progress' => function (int $dlNow, int $dlSize, array $info): void { // ... }, ]); @@ -290,10 +291,11 @@ response sequentially instead of waiting for the entire response:: Handling Exceptions ~~~~~~~~~~~~~~~~~~~ -When an HTTP error happens (status code 3xx, 4xx or 5xx) your code is expected -to handle it by checking the status code of the response. If you don't do that, -the ``getHeaders()`` and ``getContent()`` methods throw an appropriate exception:: +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 @@ -506,7 +508,7 @@ responses dynamically when it's called:: use Symfony\Component\HttpClient\Response\MockResponse; $callback = function ($method, $url, $options) { - return new MockResponse('...'; + return new MockResponse('...'); }; $client = new MockHttpClient($callback); @@ -514,7 +516,7 @@ responses dynamically when it's called:: The responses provided to the mock client don't have to be instances of ``MockResponse``. Any class implementing ``ResponseInterface`` will work (e.g. -``$this->getMockBuilder(ResponseInterface::class)->getMock()``). +``$this->createMock(ResponseInterface::class)``). However, using ``MockResponse`` allows simulating chunked responses and timeouts:: From 105ffc96be05f045a1a8483018703ba93237a197 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 29 Apr 2019 10:19:58 +0200 Subject: [PATCH 10/11] Updated some config options --- components/http_client.rst | 2 +- reference/configuration/framework.rst | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index 419053e1e77..f5bf0f30521 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -258,7 +258,7 @@ following methods:: // you can get individual info too $startTime = $response->getInfo('start_time'); -.. _http-client-streaming-responses:: +.. _http-client-streaming-responses: Streaming Responses ~~~~~~~~~~~~~~~~~~~ diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index acb65d731a2..6c766f5f745 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -90,7 +90,8 @@ Configuration * `http_client`_ - * `auth`_ + * `auth_basic`_ + * `auth_bearer`_ * `base_uri`_ * `bindto`_ * `buffer`_ @@ -690,14 +691,22 @@ consume APIs): github: # ... -auth -.... +auth_basic +.......... -**type**: ``string`` +**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'`` (a colon separates both values). +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 ........ From e70c65b0361a9b4b3f874f3e805cdb04d26654cd Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 29 Apr 2019 10:20:57 +0200 Subject: [PATCH 11/11] Commented some unfinished sections --- components/http_client.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/components/http_client.rst b/components/http_client.rst index f5bf0f30521..2a99653a1ed 100644 --- a/components/http_client.rst +++ b/components/http_client.rst @@ -227,12 +227,13 @@ making a request. Use the ``max_redirects`` setting to configure this behavior 'max_redirects' => 0, ]); -Concurrent Requests -~~~~~~~~~~~~~~~~~~~ - - +.. Concurrent Requests +.. ~~~~~~~~~~~~~~~~~~~ +.. +.. .. TODO - +.. +.. Processing Responses -------------------- @@ -315,9 +316,11 @@ 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: check the PHPdoc of the class: -.. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpClient/CachingHttpClient.php +.. +.. TODO: .. Show some example of caching requests+responses +.. +.. Scoping Client -------------- pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy