2

How do I use Guzzle 6 to create 5 async requests with the following conditions:

  • All requests start at the same time
  • I want a 500ms timeout value for all requests. If a request times out I DONT want it to interrupt other requests
  • If a request returns non-200 I DONT want it to interrupt other requests.
  • All requests are on different domains... (so I'm not sure how that fits in with the base_uri setting...

If all 5 requests return 200OK < 500ms then I want to be able to loop through their responses...

BUT, if say 2 of them have non-200 and 1 of them times out (over 500ms), I want to still be able to access the responses for the 2 successful ones.

EDIT So far everything works except timeouts are still raising an exception

Here is what I had so far:

<?php

  require __DIR__.'/../vendor/autoload.php';

  use GuzzleHttp\Client;
  use GuzzleHttp\Promise;

  $client = new Client([
    'http_errors'     => false,
    'connect_timeout' => 1.50, //////////////// 0.50
    'timeout'         => 2.00, //////////////// 1.00
    'headers' => [
      'User-Agent' => 'Test/1.0'
    ]
  ]);

  // initiate each request but do not block
  $promises = [
    'success'            => $client->getAsync('https://httpbin.org/get'),
    'success'            => $client->getAsync('https://httpbin.org/delay/1'),
    'failconnecttimeout' => $client->getAsync('https://httpbin.org/delay/2'),
    'fail500'            => $client->getAsync('https://httpbin.org/status/500'),
  ];

  // wait on all of the requests to complete. Throws a ConnectException if any
  // of the requests fail
  $results = Promise\unwrap($promises);

  // wait for the requests to complete, even if some of them fail
  $results = Promise\settle($promises)->wait();
user969068
  • 2,818
  • 5
  • 33
  • 64
Tallboy
  • 12,847
  • 13
  • 82
  • 173
  • Have you looked at [stackoverflow answer](https://stackoverflow.com/questions/19520185/how-to-perform-multiple-guzzle-requests-at-the-same-time) or [Guzzle doc](https://guzzle3.readthedocs.io/http-client/client.html#sending-requests-in-parallel)? If the requests succeed, an array of `Guzzle\Http\Message\Response` objects are returned. "A single request failure will not cause the entire pool of requests to fail. Any exceptions thrown while transferring a pool of requests will be aggregated into a `Guzzle\Common\Exception\MultiTransferException` exception. "These is the comment from their doc. – moyeen52 Nov 13 '18 at 00:24
  • Yes, I'm just trying to make it so no exception is thrown and the other requests can continue as normal. im going to update my code with the latest version i have – Tallboy Nov 13 '18 at 00:30
  • Thanks ill look at that answer, I dont use PHP often though so this is a bit of a headache. – Tallboy Nov 13 '18 at 00:32
  • So currently, if any of the 5 requests fails with 500 the entire response fails. how do i get each request to fail silently so I can just return 200 OK with the successful responses? – Tallboy Nov 13 '18 at 00:37
  • So i have everything working with `'http_errors' => false`... the onyl thing left is that timeouts still raise an exception – Tallboy Nov 13 '18 at 00:51

2 Answers2

7

Guzzle provides fulfilled and rejected callabcks in the pool. here I performed a test by your values, read more at Guzzle docs:

    $client = new Client([
        'http_errors'     => false,
        'connect_timeout' => 0.50, //////////////// 0.50
        'timeout'         => 1.00, //////////////// 1.00
        'headers' => [
          'User-Agent' => 'Test/1.0'
        ]
      ]);

$requests = function ($total) {
    $uris = [
        'https://httpbin.org/get',
        'https://httpbin.org/delay/1',
        'https://httpbin.org/delay/2',
        'https://httpbin.org/status/500',
        ];
    for ($i = 0; $i < count($uris); $i++) {
        yield new Request('GET', $uris[$i]);
    }
};

$pool = new Pool($client, $requests(8), [
    'concurrency' => 10,
    'fulfilled' => function ($response, $index) {
        // this is delivered each successful response
        print_r($index."fulfilled\n");
    },
    'rejected' => function ($reason, $index) {
        // this is delivered each failed request
        print_r($index."rejected\n");
    },
]);
// Initiate the transfers and create a promise
$promise = $pool->promise();
// Force the pool of requests to complete.
$promise->wait();

response

0fulfilled
3fulfilled
1rejected
2rejected

if you want to use your code above you can also pass response status in your $promises, here is an example:

use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
....
$client = new Client([
    'http_errors'     => false,
    'connect_timeout' => 1.50, //////////////// 0.50
    'timeout'         => 2.00, //////////////// 1.00
    'headers' => [
      'User-Agent' => 'Test/1.0'
    ]
  ]);

            $promises = [
        'success' => $client->getAsync('https://httpbin.org/get')->then(
            function (ResponseInterface $res) {
                echo $res->getStatusCode() . "\n";
            },
            function (RequestException $e) {
                echo $e->getMessage() . "\n";
                echo $e->getRequest()->getMethod();
            }
        )
        ,
        'success' => $client->getAsync('https://httpbin.org/delay/1')->then(
            function (ResponseInterface $res) {
                echo $res->getStatusCode() . "\n";
            },
            function (RequestException $e) {
                echo $e->getMessage() . "\n";
                echo $e->getRequest()->getMethod();
            }
        ),
        'failconnecttimeout' => $client->getAsync('https://httpbin.org/delay/2')->then(
            function (ResponseInterface $res) {
                echo $res->getStatusCode() . "\n";
            },
            function (RequestException $e) {
                echo $e->getMessage() . "\n";
                echo $e->getRequest()->getMethod();
            }
        ),
        'fail500' => $client->getAsync('https://httpbin.org/status/500')->then(
            function (ResponseInterface $res) {
                echo $res->getStatusCode() . "\n";
            },
            function (RequestException $e) {
                echo $e->getMessage() . "\n";
                echo $e->getRequest()->getMethod();
            }
        ),
      ];

  $results = Promise\settle($promises)->wait();
user969068
  • 2,818
  • 5
  • 33
  • 64
  • Wow this is great. I will make this answered, but before I do... while I was waiting for answers I simply did this... I wrapped `Promise\unwrap` with a `try {}` and then I catch exceptions for `GuzzleHttp\Exception\ConnectException`... is there any downside to doing it this way vs your way (which is a lot more complex)? – Tallboy Nov 13 '18 at 01:32
  • 1
    if you don't want to use `Pool` and its clearly mentioned in docs that it throws `ConnectException` so it is absolutely fine to use `try` block – user969068 Nov 13 '18 at 02:01
  • Great, thank you! ANd is it 'bad form' to access the responses like this? `$promise['state']` and `$promise['value']`?... like this: `if ($promise['state'] != 'fulfilled') { continue; } // skip failed requests ` – Tallboy Nov 13 '18 at 02:06
  • 1
    why are you accessing its properties? you can just use `$response->getStatusCode()` or `$response->getReasonPhrase()` to determine the response status – user969068 Nov 13 '18 at 02:19
  • Because I'm calling `$results = Promise\settle($promises)->wait();`. Once I loop through the `$results` I can't access `->getBody()` because the responses are wrapped in some kind of array like `['state' => 'fulfilled/rejected', 'value' => RespHere]` – Tallboy Nov 13 '18 at 02:26
  • 1
    see my updated answer for another way to get your response, I will try to avoid using properties that way. – user969068 Nov 13 '18 at 02:51
  • see here: https://pastebin.com/A1n0GBna --- Hmm I don't need to do a callback on each item. I just need to collect the data at the end from the successful responses. Here is waht I have so far... it seems to work perfect, but I'm afraid this is the wrong way to access `$promise['state']`: https://pastebin.com/A1n0GBna – Tallboy Nov 13 '18 at 02:58
  • 1
    ok....also a big issue on your code, you are using settle and unwrap together? you should only use one method, updated https://pastebin.com/2bczhK6R – user969068 Nov 13 '18 at 03:09
  • ararrghhhhhh!!! thats not clear at all from the docs.. THANK YOU. that makes so much sense... – Tallboy Nov 13 '18 at 03:21
  • Ok so it turns out that I actually need to do some work parsing the responses. I will use your 2nd part of your answer so I can do that work asyncronously (parsing the XML of all the responses). Thank you SO MUCH!!! – Tallboy Nov 13 '18 at 03:26
  • Ok @user969068 you have been so tremendously helpful. I only have 1 final question ... do I have to worry about async code bugs when I put code in these promise callbacks (in your 2nd block of code in your answer)? Lets say I have a `$output = []` array, can I push to that array inside all the `->then(...)` promise callbacks without worrying about multithreading bugs? – Tallboy Nov 13 '18 at 04:12
  • Does it perform all the promises simultaneously? Like if I have 10 promise callbacks and each one takes 1 second to do work, will the response total time be 10 second or 1 second? – Tallboy Nov 13 '18 at 04:14
0

for send multiple https request with concuurrency then you can do it as like below

see below sample code

<?php
$client = new \GuzzleHttp\Client(['base_uri' => 'http://test.com']);


$requests = function () use ($client, $product) {
    foreach ($product as $val) {
        $methodType = $val['method_type']; // Get, Post, Put 
        $urlPath = $val['url_path']; // url path eg. /search/car/{abc}
        $payload = json_decode($val['payload'], true); // your data if you wish to send in request

        yield function() use ($client, $methodType, $urlPath) {
            return $client->requestAsync($methodType, $urlPath, []);
            // return $client->requestAsync($methodType, $urlPath, [\GuzzleHttp\RequestOptions::JSON => $payload]); // for send data as a Json 
            // return $client->requestAsync($methodType, $urlPath, [\GuzzleHttp\RequestOptions::QUERY => $payload]); // for send data as a Query String in url 
        };

    }
};


$pool = new \GuzzleHttp\Pool($client, $requests(), [
    'concurrency' => 3,
    'fulfilled' => function (Response $response, $index) use (&$responses, &$successCount) {
        if ($response->getStatusCode() == 200) {
            // http status ok response
            $responses[] = json_decode($response->getBody(), true);
            $successCount++;
        } else {
            // do perform your logic for success response without 200 HTTP Status code 
        }
    },
    'rejected' => function (\GuzzleHttp\Exception\RequestException $reason, $index) use (&$failedCount) {
       // error response handle here
        if ($reason->hasResponse()) {
            $response = $reason->getResponse();
            $httpstatuscode = $response->getStatusCode();
        }
        Log::error($reason->getMessage());
        $failedCount++;
    },
]);

$pool->promise()->wait();
var_dump($responses);

for more detailed see here

Harsh Patel
  • 1,032
  • 6
  • 21