diff --git a/src/Provider/AbstractProvider.php b/src/Provider/AbstractProvider.php index d1679998..84e1b439 100644 --- a/src/Provider/AbstractProvider.php +++ b/src/Provider/AbstractProvider.php @@ -22,6 +22,7 @@ use League\OAuth2\Client\OptionProvider\OptionProviderInterface; use League\OAuth2\Client\OptionProvider\PostAuthOptionProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use League\OAuth2\Client\Provider\Exception\ResponseParsingException; use League\OAuth2\Client\Token\AccessToken; use League\OAuth2\Client\Token\AccessTokenInterface; use League\OAuth2\Client\Tool\ArrayAccessorTrait; @@ -520,6 +521,8 @@ protected function getAccessTokenRequest(array $params) * @param mixed $grant * @param array $options * @throws IdentityProviderException + * @throws ResponseParsingException if the flag $mayThrowResponseParsingException is true and + * response body cannot be parsed. * @return AccessTokenInterface */ public function getAccessToken($grant, array $options = []) @@ -613,6 +616,8 @@ public function getResponse(RequestInterface $request) * * @param RequestInterface $request * @throws IdentityProviderException + * @throws ResponseParsingException if the flag $mayThrowResponseParsingException is true and + * response body cannot be parsed. * @return mixed */ public function getParsedResponse(RequestInterface $request) @@ -665,9 +670,9 @@ protected function getContentType(ResponseInterface $response) /** * Parses the response according to its content-type header. * - * @throws UnexpectedValueException + * @throws ResponseParsingException if response body cannot be parsed. * @param ResponseInterface $response - * @return array + * @return array|string */ protected function parseResponse(ResponseInterface $response) { @@ -686,11 +691,14 @@ protected function parseResponse(ResponseInterface $response) return $this->parseJson($content); } catch (UnexpectedValueException $e) { if (strpos($type, 'json') !== false) { - throw $e; + throw new ResponseParsingException($response, $content, $e->getMessage(), $e->getCode(), $e); } + // for any other content types if ($response->getStatusCode() == 500) { - throw new UnexpectedValueException( + throw new ResponseParsingException( + $response, + $content, 'An OAuth server error was encountered that did not contain a JSON body', 0, $e @@ -774,6 +782,8 @@ public function getResourceOwner(AccessToken $token) * * @param AccessToken $token * @return mixed + * @throws ResponseParsingException if the flag $mayThrowResponseParsingException is true and + * response body cannot be parsed. */ protected function fetchResourceOwnerDetails(AccessToken $token) { diff --git a/src/Provider/Exception/ResponseParsingException.php b/src/Provider/Exception/ResponseParsingException.php new file mode 100644 index 00000000..d6f5f151 --- /dev/null +++ b/src/Provider/Exception/ResponseParsingException.php @@ -0,0 +1,79 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link http://thephpleague.com/oauth2-client/ Documentation + * @link https://packagist.org/packages/league/oauth2-client Packagist + * @link https://github.com/thephpleague/oauth2-client GitHub + */ + +namespace League\OAuth2\Client\Provider\Exception; + +use Exception; +use Psr\Http\Message\ResponseInterface; +use UnexpectedValueException; + +/** + * Exception thrown if the parser cannot parse the provider response. + */ +class ResponseParsingException extends UnexpectedValueException +{ + /** + * @var ResponseInterface + */ + protected $response; + + /** + * @var string + */ + protected $responseBody; + + /** + * @param ResponseInterface $response The response + * @param string $responseBody The response body + * @param null $message + * @param int $code + * @param Exception|null $previous + */ + public function __construct( + ResponseInterface $response, + $responseBody, + $message = null, + $code = 0, + Exception $previous = null + ) { + $this->response = $response; + $this->responseBody = $responseBody; + + if (null === $message) { + $message = sprintf('Cannot parse response body: "%s"', $responseBody); + } + + parent::__construct($message, $code, $previous); + } + + /** + * Returns the exception's response. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * Returns the exception's response body. + * + * @return string + */ + public function getResponseBody() + { + return $this->responseBody; + } +} diff --git a/test/src/Provider/AbstractProviderTest.php b/test/src/Provider/AbstractProviderTest.php index bf24ba38..cbaebaf9 100644 --- a/test/src/Provider/AbstractProviderTest.php +++ b/test/src/Provider/AbstractProviderTest.php @@ -2,25 +2,26 @@ namespace League\OAuth2\Client\Test\Provider; -use League\OAuth2\Client\OptionProvider\PostAuthOptionProvider; -use Mockery; -use ReflectionClass; -use UnexpectedValueException; -use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\ClientInterface; -use League\OAuth2\Client\Provider\AbstractProvider; -use League\OAuth2\Client\Test\Provider\Fake as MockProvider; +use GuzzleHttp\Exception\BadResponseException; use League\OAuth2\Client\Grant\AbstractGrant; -use League\OAuth2\Client\Grant\GrantFactory; use League\OAuth2\Client\Grant\Exception\InvalidGrantException; +use League\OAuth2\Client\Grant\GrantFactory; +use League\OAuth2\Client\OptionProvider\PostAuthOptionProvider; +use League\OAuth2\Client\Provider\AbstractProvider; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use League\OAuth2\Client\Provider\Exception\ResponseParsingException; +use League\OAuth2\Client\Test\Provider\Fake as MockProvider; use League\OAuth2\Client\Token\AccessToken; use League\OAuth2\Client\Token\AccessTokenInterface; use League\OAuth2\Client\Tool\RequestFactory; -use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use Mockery; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; +use ReflectionClass; +use UnexpectedValueException; class AbstractProviderTest extends TestCase { @@ -649,6 +650,32 @@ public function testParseResponseNonJsonFailure() $this->testParseResponse('', 'application/xml', null, 500); } + public function responseParsingExceptionProvider() + { + // List only combinations of content-type and status code that should lead to exception + // if the response body cannot be parsed as JSON + return [ + ['application/json', 401], + ['application/xml', 500] + ]; + } + + /** + * @dataProvider responseParsingExceptionProvider + */ + public function testResponseParsingException($type, $statusCode) + { + $exception = null; + try { + $this->testParseResponse('{13}', $type, null, $statusCode); + } catch (ResponseParsingException $exception) { + } + $this->assertInstanceOf(ResponseParsingException::class, $exception); + $response = $exception->getResponse(); + $this->assertSame($statusCode, $response->getStatusCode()); + $this->assertSame('{13}', $exception->getResponseBody()); + } + public function getAppendQueryProvider() { return [ diff --git a/test/src/Provider/Exception/ResponseParsingExceptionTest.php b/test/src/Provider/Exception/ResponseParsingExceptionTest.php new file mode 100644 index 00000000..578f82b5 --- /dev/null +++ b/test/src/Provider/Exception/ResponseParsingExceptionTest.php @@ -0,0 +1,39 @@ +body); + } + + public function testGetResponse() + { + $this->assertInstanceOf( + ResponseInterface::class, + $this->generateResponseParsingException()->getResponse() + ); + } + + public function testGetResponseBody() + { + $this->assertSame( + $this->body, + $this->generateResponseParsingException()->getResponseBody() + ); + } + + public function testMissingMessage() + { + $this->assertNotEmpty($this->generateResponseParsingException()->getMessage()); + } +}