This is a continuation from my earlier article - "PHP: decorating async GuzzleHttp calls". Hopefully it stands on its own merit, but perhaps y/day's one (ie: Thursday 17/11/2016) will offer some more context than I perhaps will repeat here. I do refer back to some of the code in that article. Go read it.
Speaking of context though, we're in the process of re-decorating (in the Decorator Pattern sense, not the shopping-at-Ikea sense) our GuzzleHttp calls, specifically when we're making async calls. Yesterday I looked at how to do request & response logging on async calls. The next mission is to deal with exceptions in our own way.
GuzzleHttp calls will raise a ClientException when an HTTP response has a 400-series status code, and a ServerException when it's in the 500 range. See "Guzzle 6 docs / Quickstart / Exceptions". That's all cool as a default, but we need to be able to map response codes to different exceptions in our adapters for one reason or another. So I need to work out how to intercept those, handle them, and throw our own exceptions. I was fairly confident it was gonna be the same as with the logging example, except using rejection handlers instead of resolution ones.
For testing I set up another quick CFML-based endpoint, which would just let me return a specific HTTP response code:
<cfheader statusCode="#URL.statusCode#">
<cfoutput>The request resulted in a #URL.statusCode# status code</cfoutput>
So whatever code I pass on the URL, it uses that as the status code for the response, and also confirms that in the body.
I figured adapter on the PHP end of things would be along these lines:
namespace me\adamcameron\testApp;
class StatusToExceptionAdapter {
private $adapter;
private $exceptionMap;
public function __construct($adapter, $exceptionMap) {
$this->adapter = $adapter;
$this->exceptionMap = $exceptionMap;
}
public function get($status){
return $this->adapter
->get($status)
->otherwise(function($exception){
$code = $exception->getCode();
if (!array_key_exists($code, $this->exceptionMap)) {
throw $exception;
}
throw new $this->exceptionMap[$code](
"A response of $code was received from the service",
$code,
$exception
);
});
}
}
Points:
- Again this adapter just decorates the adapter that does the actual "guzzling" (code in previous article);
- the decoration being a passed-in exception map.
- Where in the previous logging example I was using a resolution handler (a then call), here I'm using a rejection handler, via otherwise.
- So if the call to get results in a rejected promise (ie: an error), I do the following:
- check if the exception passed to the handler was one I have a mapping for
- if not, just rethrow the original exception
- otherwise I throw a new exception of the remapped exception type
- and pass the original exception as its previous exception.
Here's a test rig:
use \me\adamcameron\testApp\GuzzleAdapter;
use \me\adamcameron\testApp\StatusToExceptionAdapterOld;
require_once __DIR__ . "/../../vendor/autoload.php";
$endPoint = "http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=";
$guzzleAdapter = new GuzzleAdapter($endPoint);
$exceptionMap = [
400 => '\me\adamcameron\testApp\exception\BadRequestException',
503 => '\me\adamcameron\testApp\exception\ServerException'
];
$adapter = new StatusToExceptionAdapter($guzzleAdapter, $exceptionMap);
$tests = [
'non-error' => 200,
'mapped client error' => 400,
'mapped server error' => 503,
'un-mapped client error' => 404,
'un-mapped server error' => 500
];
foreach($tests as $test=>$value) {
printf("%s (%s)%s", $test, $value, PHP_EOL . PHP_EOL);
$response = $adapter->get($value);
try {
$resolvedResponse = $response->wait();
printf(
"Successful request returned: %s%s ",
(string) $resolvedResponse->getBody()
);
} catch (Exception $e){
printf("Request errored with %s%s", $e->getCode(), PHP_EOL);
printf("Class: %s%s", get_class($e), PHP_EOL);
printf("Message %s%s", $e->getMessage(), PHP_EOL);
$previous = $e->getPrevious();
if (is_null($previous)){
continue;
}
printf("Previous class %s%s", get_class($previous), PHP_EOL);
printf("Previous code %s%s", $previous->getCode(), PHP_EOL);
printf("Previous message %s%s", $previous->getMessage(), PHP_EOL);
}
finally {
echo PHP_EOL . "====================================================" . PHP_EOL . PHP_EOL;
}
}
That looks like a lot of code, but it's simple:
- initialise the Guzzle adapter
- decorate it with the exception mapping one
- I'm mapping for just a couple of response codes
- I loop over a bunch of possibilities, including:
- a 200-OK response (which is not impacted by any of this work, and should just filter through);
- both exceptions I'm remapping;
- another coupla "error" response codes I'm not remapping
- I call the web service for each of these;
- and output the results.
- Which might or might not have a wrapped "previous" exception.
C:\>php testExceptionMap.php
non-error (200)
Successful request returned: The request resulted in a 200 status code
====================================================
mapped client error (400)
Request errored with 400
Class: me\adamcameron\testApp\exception\BadRequestException
Message A response of 400 was received from the service
Previous class GuzzleHttp\Exception\ClientException
Previous code 400
Previous message Client error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=400` resulted in a `400 Bad Request` response: The request resulted in a 400 status code
====================================================
mapped server error (503)
Request errored with 503
Class: me\adamcameron\testApp\exception\ServerException
Message A response of 503 was received from the service
Previous class GuzzleHttp\Exception\ServerException
Previous code 503
Previous message Server error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=503` resulted in a `503 Service Unavailable` response: The request resulted in a 503 status code
====================================================
un-mapped client error (404)
Request errored with 404
Class: GuzzleHttp\Exception\ClientException
Message Client error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=404` resulted in a `404 Not Found` response: The request resulted in a 404 status code
====================================================
un-mapped server error (500)
Request errored with 500
Class: GuzzleHttp\Exception\ServerException
Message Server error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=500` resulted in a `500 Internal Server Error` response: The request resulted in a 500 status code
====================================================
C:\>
non-error (200)
Successful request returned: The request resulted in a 200 status code
====================================================
mapped client error (400)
Request errored with 400
Class: me\adamcameron\testApp\exception\BadRequestException
Message A response of 400 was received from the service
Previous class GuzzleHttp\Exception\ClientException
Previous code 400
Previous message Client error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=400` resulted in a `400 Bad Request` response: The request resulted in a 400 status code
====================================================
mapped server error (503)
Request errored with 503
Class: me\adamcameron\testApp\exception\ServerException
Message A response of 503 was received from the service
Previous class GuzzleHttp\Exception\ServerException
Previous code 503
Previous message Server error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=503` resulted in a `503 Service Unavailable` response: The request resulted in a 503 status code
====================================================
un-mapped client error (404)
Request errored with 404
Class: GuzzleHttp\Exception\ClientException
Message Client error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=404` resulted in a `404 Not Found` response: The request resulted in a 404 status code
====================================================
un-mapped server error (500)
Request errored with 500
Class: GuzzleHttp\Exception\ServerException
Message Server error: `GET http://cf2016.local:8516/cfml/misc/guzzleTestEndpoints/returnStatusCode.cfm?statusCode=500` resulted in a `500 Internal Server Error` response: The request resulted in a 500 status code
====================================================
C:\>
So here we see, again:
- the 200-OK response;
- both exceptions I'm remapping;
- A 404 and a 500 which I'm not remapping, so retain their Guzzle-specific exceptions
All good. But why did I sound hesitant before when I said "And... um... this all... works"? Because when I first wrote this code on Friday it didn't bloody work. No matter what I did could I stop the original Guzzle exceptions bubbling back. I started writing this up today, and hastily rewrote the code I was using on Fri for the article, and... now it bloody works! This is excellent as it's really easy, but I spent all Fri evening and Saturday working around not being able to get this working the way I wanted. Sigh. And now I can't - for the life of me - work out where I was going wrong before. Grrr.
One positive side of this though is that I did find another way of dealing with this, and it might be slightly more "semantically Guzzle", so I'll write that up tomorrow. -ish.
Anyway, I need to scarf this Guinness and go find my gate; which will hopefully have an aircraft at it, to take me back to London.
Righto.
--
Adam