Skip to content

[HttpClient] provide response body to the RetryDecider #38289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 2, 2020

Conversation

jderusse
Copy link
Member

Q A
Branch? master
Bug fix? no
New feature? yes
Deprecations? yes but for not-yet released 5.2 feature
Tickets /
License MIT
Doc PR TODO

Some servers, like AWS, does not always return standard HTTP code. The strategy needs to parse the body to take a decision.
example:

400 
x-amzn-requestid: XXXXX
content-type: application/x-amz-json-1.1
content-length: 58
date: Thu, 24 Sep 2020 11:17:35 GMT
connection: close

{"__type":"ThrottlingException","message":"Rate exceeded"}

This PR update the RetryDeciderInterface interface to let the decider notifying the Client when it need the body to take a decision. in that case, the Client, fetch te client, and call again the decider with the full body.

usage

class Decider implements RetryDeciderInterface {
    public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseCode, array $responseHeaders, ?string $responseContent, \Throwable $throwable = null): ?bool
    {
        if (null !== $throwable) {
            return true;
        }
        if (in_array($responseCode, [423, 425, 429, 500, 502, 503, 504, 507, 510])) {
            return true;
        }
        if (
            $responseCode !== 400
            || $headers['content-type'][0] ?? null !== 'application/x-amz-json-1.1' 
            || (int) $headers['content-length'][0] ?? '0' > 1024
        ) {
            return false;
        }
        if (null === $responseContent) {
            return null; // null mean no decision taken and need to be called again with the body
        }

        $data = json_decode($responseContent, true);

        return $data['__type'] ?? '' === 'ThrottlingException';
    }
}

@jderusse jderusse changed the title [HtppClient] provide body to retry decider [HtppClient] provide response body to the RetryDecider Sep 24, 2020
@jderusse jderusse force-pushed the retry-body branch 3 times, most recently from 1c29567 to cde9938 Compare September 24, 2020 11:56
@nicolas-grekas nicolas-grekas added this to the next milestone Sep 24, 2020
@nicolas-grekas nicolas-grekas changed the title [HtppClient] provide response body to the RetryDecider [HttpClient] provide response body to the RetryDecider Sep 24, 2020
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content-type: application/x-amz-json-1.1

for my understanding, this means you're going to add a decider in async-aws that will chck both the status code and this specific content-type to decide to wait for the body, isn't it?

@jderusse
Copy link
Member Author

jderusse commented Oct 1, 2020

for my understanding, this means you're going to add a decider in async-aws that will chck both the status code and this specific content-type to decide to wait for the body, isn't it?

yes. The decider should avoid as possible downloading the body. It can take a quick decision when statusCode is 2xx, 429, 5xx... Or content-type is unknow or content-length is too big.

but in some case (status = 400 + contentType = json + content-length < XXX) then it'll ask to the client to provide the body to take the decision.

@nicolas-grekas nicolas-grekas force-pushed the retry-body branch 2 times, most recently from d4da9b2 to d0844c2 Compare October 1, 2020 15:31
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I force-pushed a patch on AsyncResponse and confirmed it works)

@jderusse
Copy link
Member Author

jderusse commented Oct 1, 2020

(I force-pushed a patch on AsyncResponse and confirmed it works)

Confirmed, it works. I pushed a commit about fixing wrong exception namespace

@fabpot
Copy link
Member

fabpot commented Oct 2, 2020

Thank you @jderusse.

@bohanyang
Copy link
Contributor

The body is read once so getContent don't work after that:

PHP Fatal error:  Uncaught Symfony\Component\HttpClient\Exception\TransportException: Cannot get the content of the response twice: buffering is disabled.

Sorry if my usage is wrong...

<?php

declare(strict_types=1);

use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
use Symfony\Component\HttpClient\RetryableHttpClient;

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

$client = new RetryableHttpClient(new CurlHttpClient(), new HttpStatusCodeDecider(), new ExponentialBackOff(), 1);
$resp = $client->request('GET', 'https://httpbin.org/status/429');
var_dump($resp->getContent(false));

nicolas-grekas added a commit that referenced this pull request Oct 2, 2020
…derusse)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[HttpClient] Fix exception triggered with AsyncResponse

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | #38289 (comment)
| License       | MIT
| Doc PR        | /

When a request is replayed with the AsyncResponse and the 2nd request failed, users get an exception even when they pass `$throw=false`.

In fact, there are 2 exceptions:

1. Cannot get the content of the response twice: buffering is disabled.

https://github.com/symfony/symfony/blob/73ca97c97ab4c4fc9a7bca5fb4339fca22e0cfa1/src/Symfony/Component/HttpClient/Response/CommonResponseTrait.php#L51-L68

Because CommonResponseTrait::$content is null and `self::stream` yield only the LastChunk. The content is stays null

2. HTTP/2 429  returned for "https://httpbin.org/status/429"

https://github.com/symfony/symfony/blob/73ca97c97ab4c4fc9a7bca5fb4339fca22e0cfa1/src/Symfony/Component/HttpClient/Response/AsyncResponse.php#L209-L225

Because on the second request passthru is null, it didn't disable the the exception thrown on destruct for the wrapped response.
This

Commits
-------

3aedb51 [HttpClient] Fix exception triggered with AsyncResponse
symfony-splitter pushed a commit to symfony/http-client that referenced this pull request Oct 2, 2020
…derusse)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[HttpClient] Fix exception triggered with AsyncResponse

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | symfony/symfony#38289 (comment)
| License       | MIT
| Doc PR        | /

When a request is replayed with the AsyncResponse and the 2nd request failed, users get an exception even when they pass `$throw=false`.

In fact, there are 2 exceptions:

1. Cannot get the content of the response twice: buffering is disabled.

https://github.com/symfony/symfony/blob/73ca97c97ab4c4fc9a7bca5fb4339fca22e0cfa1/src/Symfony/Component/HttpClient/Response/CommonResponseTrait.php#L51-L68

Because CommonResponseTrait::$content is null and `self::stream` yield only the LastChunk. The content is stays null

2. HTTP/2 429  returned for "https://httpbin.org/status/429"

https://github.com/symfony/symfony/blob/73ca97c97ab4c4fc9a7bca5fb4339fca22e0cfa1/src/Symfony/Component/HttpClient/Response/AsyncResponse.php#L209-L225

Because on the second request passthru is null, it didn't disable the the exception thrown on destruct for the wrapped response.
This

Commits
-------

3aedb51dd8 [HttpClient] Fix exception triggered with AsyncResponse
@nicolas-grekas nicolas-grekas modified the milestones: next, 5.2 Oct 5, 2020
@fabpot fabpot mentioned this pull request Oct 5, 2020
@jderusse jderusse deleted the retry-body branch October 15, 2020 10:01
sadafrangian3 pushed a commit to sadafrangian3/Dependency-Injection-http-client that referenced this pull request Nov 2, 2022
…derusse)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[HttpClient] Fix exception triggered with AsyncResponse

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | symfony/symfony#38289 (comment)
| License       | MIT
| Doc PR        | /

When a request is replayed with the AsyncResponse and the 2nd request failed, users get an exception even when they pass `$throw=false`.

In fact, there are 2 exceptions:

1. Cannot get the content of the response twice: buffering is disabled.

https://github.com/symfony/symfony/blob/73ca97c97ab4c4fc9a7bca5fb4339fca22e0cfa1/src/Symfony/Component/HttpClient/Response/CommonResponseTrait.php#L51-L68

Because CommonResponseTrait::$content is null and `self::stream` yield only the LastChunk. The content is stays null

2. HTTP/2 429  returned for "https://httpbin.org/status/429"

https://github.com/symfony/symfony/blob/73ca97c97ab4c4fc9a7bca5fb4339fca22e0cfa1/src/Symfony/Component/HttpClient/Response/AsyncResponse.php#L209-L225

Because on the second request passthru is null, it didn't disable the the exception thrown on destruct for the wrapped response.
This

Commits
-------

3aedb51dd8 [HttpClient] Fix exception triggered with AsyncResponse
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants