Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Modern And Fast HttpClient

BRUNO SOUZA
December 04, 2020

The Modern And Fast HttpClient

Due to the growing number of companies using approaches like APIs and
services oriented architecture the need and use of a http client is
getting even more evidence. There are many options been offering in
the IT market. However, the Symfony decide to write their own client and this one is different from all the others, since its conception.
Understand the main differences between this client and the others.
Why it's faster and modern, the options and advantages to use and
structure that allows you to have a nice http client component without
the need to do a great effort and changes in your application.

BRUNO SOUZA

December 04, 2020
Tweet

More Decks by BRUNO SOUZA

Other Decks in Technology

Transcript

  1. WHO AM I? Web Developer and Software Engineer since 2011.

    Brazilian living in Ireland. Data Science and DevOps student. Member of PHP DF and PHP Dublin community. Speaker, Father and Guitar player Bruno Souza @Bruno_HSouza
  2. What is the HttpClient? Installation and Configuration Requests Responses 01

    02 03 04 CONTENT 05 Handling Exceptions 06 Testing the Component 07 New Features on Symfony 5.2
  3. WHAT IS THE SYMFONY HTTP CLIENT? Launched as experimental in

    Symfony 4.3 First official release with the Symfony 4.4 PSR-7 and PSR-18 compatibility Support HTTP and HTTP/2 Request and Response caching; Scoped Clients; HTTP Authentications;
  4. # config/packages/framework.yaml framework: http_client: http_version: '2.0' Standalone component: public function

    getClient() { $this->client = HttpClient::create([ 'base_uri' => 'http://api.openweathermap.org', 'http_version' => '2.0', ]); } Configuration Symfony Framework:
  5. HTTP/2 • libcurl: ^7.36 • amphp/http-client: ^4.2 • HTTP/2 Server

    Push: libcurl >= 7.61 and PHP >= 7.2.17 || 7.3.4
  6. HTTP/2 public function getAmpHttpClient() { $client = new AmpHttpClient([ 'base_uri'

    => 'https://http2.pro' ]); $response = $client->request( 'GET', '/api/v1' ); var_dump($response->getStatusCode()); dd($response->getInfo()); } public function getCurlHttpClient() { $client = new CurlHttpClient([ 'base_uri' => 'https://http2.pro' ]); $response = $client->request( 'GET', '/api/v1' ); var_dump($response->getStatusCode()); dd($response->getInfo()); }
  7. public function __construct(HttpClientInterface $client) { $this->client = $client; } Basic

    Usage Using a Symfony Framework application: • Check the service container: bin/console debug:autowiring; • Using Dependency Injection (Inversion) to set the class in the constructor:
  8. Create static client /** * @param array $defaultOptions Default request's

    options * @param int $maxHostConnections The maximum number of connections to a single host * @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue * * @see HttpClientInterface::OPTIONS_DEFAULTS for available options */ public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
  9. Client transport types public function getClient() { // uses the

    native PHP streams $this->client = new NativeHttpClient(); } public function getClient() { // uses the cURL PHP extension $this->client = new CurlHttpClient(); }
  10. # config/packages/framework.yaml framework: http_client: scoped_clients: # only requests matching scope

    will use these options openweather: scope: 'http://api.openweathermap.org' headers: Accept: 'application/json' base_uri: 'http://api.openweathermap.org' query: key: 'appid' value: "%env(OPENWEATHERMAP_ID)%" apifirst: base_uri: 'https://api.first.org' headers: Accept: 'application/json' query: key: 'access' value: 'full' Configuration - Scoping Client
  11. Requests $response = $this->client->request( 'GET', '/data/v1/countries', [ 'headers' => ['Content-Type'

    => 'application/json'], 'query' => [ 'limit' => 251, ], ] ); return $response->getContent();
  12. public const OPTIONS_DEFAULTS = [ 'auth_basic' => null, // array|string

    - an array containing the username as first value, and optionally the // password as the second one; or string like username:password - enabling HTTP Basic // authentication (RFC 7617) 'auth_bearer' => null, // string - a token enabling HTTP Bearer authorization (RFC 6750) 'query' => [], // string[] - associative array of query string values to merge with the request's URL 'headers' => [], // iterable|string[]|string[][] - headers names provided as keys or as part of values 'body' => '', // array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string // smaller than the amount requested as argument; the empty string signals EOF; if // an array is passed, it is meant as a form payload of field names and values 'json' => null, // mixed - if set, implementations MUST set the "body" option to the JSON-encoded // value and set the "content-type" header to a JSON-compatible value if it is not // explicitly defined in the headers option - typically "application/json" 'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that // MUST be available via $response->getInfo('user_data') - not used internally 'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower than or equal to 0 // means redirects should not be followed; "Authorization" and "Cookie" headers MUST // NOT follow except for the initial host name 'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0 'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2 'buffer' => true, // bool|resource|\Closure - whether the content of the response should be buffered or not, // or a stream resource where the response body should be written, // or a closure telling if/where the response should be buffered based on its headers 'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort // the request; it MUST be called on DNS resolution, on arrival of headers and on // completion; it SHOULD be called on upload/download of data and at least 1/s 'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution 'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored 'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached 'timeout' => null, // float - the idle timeout - defaults to ini_get('default_socket_timeout') ... Options FTW #vendor/symfony/http-client-contracts/HttpClientInterface.php 'max_duration' => 0, 'bindto' => '0', 'verify_peer' => true, 'verify_host' => true, 'cafile' => null, 'capath' => null, 'local_cert' => null, 'local_pk' => null, 'passphrase' => null, 'ciphers' => null, 'peer_fingerprint' => null, 'capture_peer_cert_chain' => false, 'extra' => [],
  13. JSON Payload $response = $this->client->request( 'POST', '/data/v1/address', [ 'headers' =>

    ['Content-Type' => 'application/json'], 'auth_bearer' => $token, 'json' => $requestContent, ] ); return $response->getContent();
  14. Response to Array $query = [ 'query' => [ 'id'

    => $cityId, 'appid' => $this->openweathermap_id ] ]; $response = $this->client->request('GET', '/data/2.5/weather', $query); return json_decode($response->getBody()); $query = [ 'query' => [ 'id' => $cityId, 'appid' => $this->openweathermap_id ] ]; $response = $this->client->request('GET', '/data/2.5/weather', $query); return $response->toArray();
  15. Multiplexing “ A way of sending multiple signals or streams

    of information over a communications link at the same time in the form of a single, complex signal; the receiver recovers the separate signals, a process called demultiplexing” Margaret Rouse, Whatis.com
  16. Multiplexing in HttpClient $url = 'https://.../ubuntu-18.04.1-desktop-amd64.iso'; $response = $this->httpClient->request('GET', $url);

    // Responses are lazy: this code is executed as soon as headers are received if (200 !== $response->getStatusCode()) { throw new \Exception('...'); } // response chunks implement Symfony\Contracts\HttpClient\ChunkInterface $fileHandler = fopen('/ubuntu.iso', 'w'); foreach ($this->httpClient->stream($response, 0.0) as $chunk) { if ($chunk->isFirst()) { // headers of $response just arrived $response->getHeaders(); } elseif ($chunk->isLast()) { // the full content of $response just completed fwrite($fileHandler, $chunk->getContent()); } else { // $chunk->getContent() will return a piece $chunk->getContent(); } }
  17. $startTime = microtime(true); try { $responses = []; for ($i

    = 0; $i < 379; ++$i) { $uri = "https://http2.akamai.com/demo/tile-$i.png"; $responses[] = $this->httpClient->request('GET', $uri); } $content = ''; foreach ($responses as $response) { $content = $response->getContent(); } $endTime = microtime(true); return 'Process Time 379 requests with HTTP2: ' . $endTime - $startTime; } catch (\Exception $exception) { throw $exception; } Concurrent Requests
  18. Handling Exceptions #vendor/symfony/http-client-contracts/ResponseInterface.php /** * Gets the response body decoded

    as array, typically from a JSON payload. * * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes * * @throws DecodingExceptionInterface When the body cannot be decoded to an array * @throws TransportExceptionInterface When a network error occurs * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached * @throws ClientExceptionInterface On a 4xx when $throw is true * @throws ServerExceptionInterface On a 5xx when $throw is true */ public function toArray(bool $throw = true): array;
  19. Handling Exceptions #vendor/symfony/http-client-contracts/ResponseInterface.php /** * Gets the response body decoded

    as array, typically from a JSON payload. * * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes * * @throws DecodingExceptionInterface When the body cannot be decoded to an array * @throws TransportExceptionInterface When a network error occurs * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached * @throws ClientExceptionInterface On a 4xx when $throw is true * @throws ServerExceptionInterface On a 5xx when $throw is true */ public function toArray(bool $throw = true): array;
  20. Handling Exceptions return $response->getContent(); } catch (ClientExceptionInterface $e) { $this->logger->error($e->getMessage(),

    $e->getResponse()->toArray()); } catch (RedirectionExceptionInterface $e) { $this->logger->warning($e->getMessage(), $e->getResponse()->toArray()); } catch (ServerExceptionInterface $e) { $this->logger->error($e->getMessage(), [ 'status' => $e->getResponse()->getStatusCode(), 'content' => $e->getResponse()->toArray(), 'info' => $e->getResponse()->getInfo() ]); } catch (TransportExceptionInterface $e) { // we do not have response in this case as should be a network issue $this->logger->error($e->getMessage()); }
  21. public function httpResponseProvider(): \Generator { yield 'response-success' => [ (function()

    { $body = file_get_contents('data/weather-response.json'); $info = (array) file_get_contents('data/weather-response-info.txt'); return new MockResponse($body, $info); })() ]; yield 'response-failed' => [ (function() { $body = file_get_contents('data/weather-response-failed.json'); $info = (array) file_get_contents('data/weather-response-failed-info.txt'); return new MockResponse($body, $info); })() ]; } Testing the component /** * @dataProvider httpResponseProvider */ public function testGetWeatherFromApi(array $response) :void { $client = new MockHttpClient([$response]); $response = $client->request( 'GET', 'http://api.openweathermap.org/data/2.5/weather', [ 'query' => [ 'name' => 'Dublin', 'appid' => bin2hex('app-id') ], 'timeout' => 5.0 ] ); $content = json_decode($response->getContent(), true); $this->assertEquals(200, $response->getStatusCode()); $this->assertArrayHasKey('name', $content); $this->assertEquals('Dublin', $content['name']); $this->assertArrayHasKey('weather', $content); $this->assertArrayHasKey('main', $content); $this->assertArrayHasKey('temp', $content['main']); $this->assertEquals(1, $client->getRequestsCount()); }
  22. New Features in Symfony 5.2 - Retry Failed framework: http_client:

    default_options: retry_failed: # retry_strategy: app.custom_strategy http_codes: 0: [ 'GET', 'HEAD' ] # retry network errors if request method is GET or HEAD 429: true # retry all responses with 429 status code 500: [ 'GET', 'HEAD' ] max_retries: 2 delay: 1000 multiplier: 3 max_delay: 5000 jitter: 0.3 #config/packages/framework.yaml
  23. New Features in Symfony 5.2 - Retry Failed public const

    IDEMPOTENT_METHODS = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']; public const DEFAULT_RETRY_STATUS_CODES = [ 0 => self::IDEMPOTENT_METHODS, // for transport exceptions 423, 425, 429, 500 => self::IDEMPOTENT_METHODS, 502, 503, 504 => self::IDEMPOTENT_METHODS, 507 => self::IDEMPOTENT_METHODS, 510 => self::IDEMPOTENT_METHODS, ]; #Retry/GenericRetryStrategy.php
  24. New Features in Symfony 5.2 - Retry Failed $response =

    $this->client->request( 'GET', 'http://httpstat.us/500', [ 'headers' => ['Content-type' => 'application/json'] ] ); echo $response->getStatusCode(); dd($response->getInfo());
  25. class AsyncDecoratorTraitExampleService implements HttpClientInterface { use AsyncDecoratorTrait; public function request(string

    $method, string $url, array $options = []): ResponseInterface { return new AsyncResponse($method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) { // do what you want with chunks, e.g. split them // in smaller chunks, group them, skip some, etc. return $context->getResponse(); }); } New Features in Symfony 5.2 - Async Decorator Trait
  26. New Features in Symfony 5.2 - Event Source Http Client

    use Symfony\Component\HttpClient\Chunk\ServerSentEvent; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Component\HttpClient\HttpClient; $client = new EventSourceHttpClient($client); // this is the URL that sends server events $source = $client->connect('https://localhost:8080/events'); while ($source) { foreach ($client->stream($source, 2) as $r => $chunk) { // this is a special ServerSentEvent chunk holding the pushed message if ($chunk instanceof ServerSentEvent) { // do something with the message } } }
  27. New Features in Symfony 5.2 - Extra Curl Options $response

    = $this->client->request( 'GET', '/data/2.5/weather', [ 'query' => [ 'id' => $cityId, ], 'extra' => [ 'curl' => [ CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V6 ] ] ...