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. THE MODERN
    AND FAST HTTP
    CLIENT

    View full-size slide

  2. 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

    View full-size slide

  3. 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

    View full-size slide

  4. WHAT IS THE SYMFONY HTTP CLIENT?

    View full-size slide

  5. 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;

    View full-size slide

  6. Installation Configuration

    View full-size slide

  7. Installation
    composer require symfony/http-client

    View full-size slide

  8. App Example
    https://api.first.org/
    https://openweathermap.org/api

    View full-size slide

  9. # 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:

    View full-size slide

  10. 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

    View full-size slide

  11. 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());
    }

    View full-size slide

  12. 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:

    View full-size slide

  13. 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

    View full-size slide

  14. 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();
    }

    View full-size slide

  15. # 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

    View full-size slide

  16. public function __construct(HttpClientInterface $apifirst)
    {
    $this->client = $apifirst;
    }
    bin/console debug:autowiring:
    Scoped Clients

    View full-size slide

  17. dump($client):
    Scoped Clients

    View full-size slide

  18. Creating Requests

    View full-size slide

  19. Requests
    $response = $this->client->request(
    'GET',
    '/data/v1/countries',
    [
    'headers' => ['Content-Type' => 'application/json'],
    'query' => [
    'limit' => 251,
    ],
    ]
    );
    return $response->getContent();

    View full-size slide

  20. 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' => [],

    View full-size slide

  21. JSON Payload
    $response = $this->client->request(
    'POST',
    '/data/v1/address',
    [
    'headers' => ['Content-Type' => 'application/json'],
    'auth_bearer' => $token,
    'json' => $requestContent,
    ]
    );
    return $response->getContent();

    View full-size slide

  22. Receiving Responses

    View full-size slide

  23. Response Interface

    View full-size slide

  24. Response Info

    View full-size slide

  25. 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();

    View full-size slide

  26. Multiplexing
    Concurrent
    Requests

    View full-size slide

  27. 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

    View full-size slide

  28. 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();
    }
    }

    View full-size slide

  29. $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

    View full-size slide

  30. Concurrent Requests (Response time)

    View full-size slide

  31. Handling Exceptions

    View full-size slide

  32. 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;

    View full-size slide

  33. 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;

    View full-size slide

  34. 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());
    }

    View full-size slide

  35. 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());
    }

    View full-size slide

  36. New Features Symfony 5.2

    View full-size slide

  37. 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

    View full-size slide

  38. 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

    View full-size slide

  39. 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());

    View full-size slide

  40. New Features in Symfony 5.2 - Retry Failed

    View full-size slide

  41. 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

    View full-size slide

  42. 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
    }
    }
    }

    View full-size slide

  43. 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
    ]
    ]
    ...

    View full-size slide

  44. References
    https:/
    /symfony.com/doc/current/http_client.html
    https:/
    /github.com/symfony
    https:/
    /github.com/brunohsouza/http-client-examples
    That Podcast With Beau And Dave

    View full-size slide