Skip to content
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

Retry logic broken in RetryDeciderTrait for $httpRetryMessages #1813

Closed
paslandau opened this issue Apr 5, 2019 · 2 comments
Closed

Retry logic broken in RetryDeciderTrait for $httpRetryMessages #1813

paslandau opened this issue Apr 5, 2019 · 2 comments
Assignees
Labels
api: core priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.

Comments

@paslandau
Copy link

paslandau commented Apr 5, 2019

Environment details

  • OS: any
  • PHP version: any
  • Package name and version: core

Description

The default retry logic of RetryDeciderTrait is broken. The code assumes that the exception message contains a valid json string that can be examined for a reason key.

This assumption is incorrect, because the underlying Guzzle client will provide a "pretty" exception message that is also truncated (see guzzle/guzzle#2185 ). The actual content of the response can be retrieved via $ex->getResponse()->getBody()->getContents();.

This issue mainly effects the RequestWrapper as it the RetryDeciderTrait is used here to retrieve the default retry function.

Steps to reproduce

The following unit test reproduces the problem. Please note that I deliberatly use the Guzzle Mock Handler in order to get an actual RequestException from Guzzle, triggered by a 4xx/5xx status code:

    /**
     * @dataProvider shouldRetryDefaultRetryableErrorsDataProvider
     */
    public function testShouldRetryDefaultRetryableErrors(array $responses, \Exception $e = null)
    {
        $mock = new MockHandler();
        foreach ($responses as $response) {
            $mock->append($response);
        }

        // add valid respone
        $mock->append(new Response(200));

        $handler     = HandlerStack::create($mock);
        $client      = new Client(['handler' => $handler]);
        $httpHandler = HttpHandlerFactory::build($client);

        $requestWrapper = new RequestWrapper([
            'credentialsFetcher' => new AnonymousCredentials(),
            'httpHandler'        => $httpHandler,
            'restDelayFunction'  => function ($delay) {
            },
        ]);

        if ($e !== null) {
            $this->expectException(get_class($e));
            $this->expectExceptionMessage($e->getMessage());
        }
        $requestWrapper->send(new Request('GET', 'http://www.example.com'));
        $this->assertTrue(true);
    }

    public function shouldRetryDefaultRetryableErrorsDataProvider()
    {
        // Note:
        // Examples taken from RetryDeciderTrait
        return [
            "reason: 500"                   => [
                "responses" => [
                    new Response(500),
                ],
                "exception" => null,
            ],
            "reason: 502"                   => [
                "responses" => [
                    new Response(502),
                ],
                "exception" => null,
            ],
            "reason: 503"                   => [
                "responses" => [
                    new Response(503),
                ],
                "exception" => null,
            ],
            "reason: rateLimitExceeded"     => [
                "responses" => [
                    new Response(429, [], json_encode([
                        "error" => [
                            "errors" => [
                                [
                                    "reason" => "rateLimitExceeded",
                                ],
                            ],
                        ],
                    ])),
                ],
                "exception" => null,
            ],
            "reason: userRateLimitExceeded" => [
                "responses" => [
                    new Response(429, [], json_encode([
                        "error" => [
                            "errors" => [
                                [
                                    "reason" => "userRateLimitExceeded",
                                ],
                            ],
                        ],
                    ])),
                ],
                "exception" => null,
            ],
            "non-retryable error"           => [
                "responses" => [
                    new Response(400, [], json_encode([
                        "error" => [
                            "errors" => [
                                [
                                    "reason" => "badRequest",
                                ],
                            ],
                        ],
                    ])),
                ],
                "exception" => new BadRequestException("badRequest"),
            ],
        ];
    }

Proposed solution

The RetryDeciderTrait should check if the Exception is an instance of a Guzzle RequestException and use the reponse body in that case to check for the error:

            $message = $ex->getMessage();
            if ($ex instanceof RequestException) {
                $message = $ex->getResponse()->getBody()->getContents();
            }
            $message = json_decode($message, true);

instead of

            $message = json_decode($ex->getMessage(), true);

I would provide a correponsing pull request if this gets accepted.

PS: I guess #1273 is actually already resolved?

@dwsupplee dwsupplee added api: core priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Apr 5, 2019
@dwsupplee dwsupplee self-assigned this Apr 5, 2019
@dwsupplee
Copy link
Contributor

@paslandau your proposed solution is exactly what we need.

A PR would be wonderful, but I am also happy to hop on getting this resolved as I'd like to get this fixed ASAP. Please let me know.

@paslandau
Copy link
Author

@dwsupplee

Unfortunately currently no access to a laptop - feel free do fix it yourself. I'll get that contribution another time ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: core priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Projects
None yet
Development

No branches or pull requests

2 participants