frankkessler / guzzle-oauth2-middleware

Guzzle middleware to perform oauth2 connections - Guzzle 6
MIT License
13 stars 14 forks source link

Request flow broken when request URL does not respond in time #18

Open mirfilip opened 7 years ago

mirfilip commented 7 years ago

Hi, so I wanted to test my client that uses this library for Oauth2 flow and I noticed couple of problems.

Background

For reproduction sake, let's start a dummy webserver using netcat:

SLEEP_FOR=1; while true; do printf "##############\n"; { echo -e 'HTTP/1.1 200 OK\r\nContent-type: application/json\r\nContent-length: 61\r\n'; sleep $SLEEP_FOR; printf '{"access_token": "a token", "expires":"2017-07-09T13:28:03Z"}'; } | nc -l 9005; printf "\n##############\n"; done

Every request to localhost:9005 will first establish connection immediately, wait for 1s and print the answer. Actual body response doesn't matter.

Don't mind my example above. The way to simulate delays in webserver is irrelevant; use what you want.

Success case

Let's configure Oauth2Client with the following config:

$client = new Oauth2Client(
            [
                RequestOptions::AUTH => 'oauth2',
                RequestOptions::CONNECT_TIMEOUT => 5,
                RequestOptions::TIMEOUT => 10,
            ]
        );
$clientCredentialsGrant = new ClientCredentials([
            'client_id' => 'some_client_id',
            'client_secret' => 'some_client_secret',
            'token_url' => 'localhost:9005/token',
            'auth_location' => 'headers',
            'body_type' => 'json',
        ]);

        $client->setGrantType($clientCredentialsGrant);

It works. This lib is able to parse dummy response from the dummy webserver and the actual call for resources can be accomplished.

Failure case

When you start a webserver to sleep for more than 10 seconds between sending headers and body (that's the timeout for the whole request), there is a problem.

try {
    $request = new Request(
        'POST',
        'localhost:9005/resource',
        [],
        '{}'
    );
    $options = [
        RequestOptions::AUTH => 'oauth2',
        RequestOptions::CONNECT_TIMEOUT => 5,
        RequestOptions::TIMEOUT => 10,
    ];
    $response = $client->send($request, $options);

    $responseCode = $response->getStatusCode();
} catch (\Exception $e) {
}

Note no retries are passed, so RetryModifyRequestMiddleware sets retries to 0. What happens is Symfony\Component\Debug\Exception\ContextErrorException is thrown with:

Catchable Fatal Error: Argument 2 passed to Frankkessler\Guzzle\Oauth2\Oauth2Client::Frankkessler\Guzzle\Oauth2\{closure}() must implement interface Psr\Http\Message\RequestInterface, none given in vendor/frankkessler/guzzle-oauth2-middleware/src/Oauth2Client.php on line 69
(...)

What's wrong

Not sure yet. Something breaks in promise resolution. Any help with debugging will be greatly appreciated.

EDIT: The same actually happens for the main request too. I set up a proof of concept using bare Guzzle 6.2.1 and it works as expected. This is clearly an issue with the way middlewares are hooked up.

EDIT 2: If you find a dummy webserver using nc misbehaving, here's the same using socat:

SLEEP_FOR=5; socat -vv TCP-LISTEN:9006,crlf,reuseaddr,fork SYSTEM:"echo 'HTTP/1.0 200'; echo 'Content-Type: application/json'; echo 'Content-length: 2'; echo; sleep $SLEEP_FOR; echo '{}'"
vanpet commented 6 years ago

Any news on this? I have the same problem and don't how how to fix it.