Skip to content

Commit 342ecf6

Browse files
authored
Merge pull request #169 from SparkPost/issue-168
Issue-168: Optional automatic retry on 5xx
2 parents 4de9c54 + 7beccde commit 342ecf6

File tree

4 files changed

+151
-4
lines changed

4 files changed

+151
-4
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ $sparky = new SparkPost($httpClient, ['key'=>'YOUR_API_KEY']);
102102
* Type: `Boolean`
103103
* Default: `true`
104104
* `async` defines if the `request` function sends an asynchronous or synchronous request. If your client does not support async requests set this to `false`
105+
* `options.retries`
106+
* Required: No
107+
* Type: `Number`
108+
* Default: `0`
109+
* `retries` controls how many API call attempts the client makes after receiving a 5xx response
105110
* `options.debug`
106111
* Required: No
107112
* Type: `Boolean`
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Examples\Templates;
4+
5+
require dirname(__FILE__).'/../bootstrap.php';
6+
7+
use SparkPost\SparkPost;
8+
use GuzzleHttp\Client;
9+
use Http\Adapter\Guzzle6\Client as GuzzleAdapter;
10+
11+
$httpClient = new GuzzleAdapter(new Client());
12+
13+
$sparky = new SparkPost($httpClient, ["key" => "YOUR_API_KEY", "retries" => 3]);
14+
15+
$promise = $sparky->request('GET', 'message-events', [
16+
'campaign_ids' => 'CAMPAIGN_ID',
17+
]);
18+
19+
/**
20+
* If this fails with a 5xx it will have failed 4 times
21+
*/
22+
try {
23+
$response = $promise->wait();
24+
echo $response->getStatusCode()."\n";
25+
print_r($response->getBody())."\n";
26+
} catch (\Exception $e) {
27+
echo $e->getCode()."\n";
28+
echo $e->getMessage()."\n";
29+
30+
if ($e->getCode() >= 500 && $e->getCode() <= 599) {
31+
echo "Wow, this failed epically";
32+
}
33+
}

lib/SparkPost/SparkPost.php

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class SparkPost
4141
'version' => 'v1',
4242
'async' => true,
4343
'debug' => false,
44+
'retries' => 0
4445
];
4546

4647
/**
@@ -97,13 +98,29 @@ public function syncRequest($method = 'GET', $uri = '', $payload = [], $headers
9798
$requestValues = $this->buildRequestValues($method, $uri, $payload, $headers);
9899
$request = call_user_func_array(array($this, 'buildRequestInstance'), $requestValues);
99100

101+
$retries = $this->options['retries'];
100102
try {
101-
return new SparkPostResponse($this->httpClient->sendRequest($request), $this->ifDebug($requestValues));
103+
if ($retries > 0) {
104+
$resp = $this->syncReqWithRetry($request, $retries);
105+
} else {
106+
$resp = $this->httpClient->sendRequest($request);
107+
}
108+
return new SparkPostResponse($resp, $this->ifDebug($requestValues));
102109
} catch (\Exception $exception) {
103110
throw new SparkPostException($exception, $this->ifDebug($requestValues));
104111
}
105112
}
106113

114+
private function syncReqWithRetry($request, $retries)
115+
{
116+
$resp = $this->httpClient->sendRequest($request);
117+
$status = $resp->getStatusCode();
118+
if ($status >= 500 && $status <= 599 && $retries > 0) {
119+
return $this->syncReqWithRetry($request, $retries-1);
120+
}
121+
return $resp;
122+
}
123+
107124
/**
108125
* Sends async request to SparkPost API.
109126
*
@@ -120,12 +137,28 @@ public function asyncRequest($method = 'GET', $uri = '', $payload = [], $headers
120137
$requestValues = $this->buildRequestValues($method, $uri, $payload, $headers);
121138
$request = call_user_func_array(array($this, 'buildRequestInstance'), $requestValues);
122139

123-
return new SparkPostPromise($this->httpClient->sendAsyncRequest($request), $this->ifDebug($requestValues));
140+
$retries = $this->options['retries'];
141+
if ($retries > 0) {
142+
return new SparkPostPromise($this->asyncReqWithRetry($request, $retries), $this->ifDebug($requestValues));
143+
} else {
144+
return new SparkPostPromise($this->httpClient->sendAsyncRequest($request), $this->ifDebug($requestValues));
145+
}
124146
} else {
125147
throw new \Exception('Your http client does not support asynchronous requests. Please use a different client or use synchronous requests.');
126148
}
127149
}
128150

151+
private function asyncReqWithRetry($request, $retries)
152+
{
153+
return $this->httpClient->sendAsyncRequest($request)->then(function($response) use ($request, $retries) {
154+
$status = $response->getStatusCode();
155+
if ($status >= 500 && $status <= 599 && $retries > 0) {
156+
return $this->asyncReqWithRetry($request, $retries-1);
157+
}
158+
return $response;
159+
});
160+
}
161+
129162
/**
130163
* Builds request values from given params.
131164
*
@@ -252,7 +285,7 @@ public function setHttpClient($httpClient)
252285
}
253286

254287
$this->httpClient = $httpClient;
255-
288+
256289
return $this;
257290
}
258291

@@ -283,7 +316,7 @@ public function setOptions($options)
283316
$this->options[$option] = $value;
284317
}
285318
}
286-
319+
287320
return $this;
288321
}
289322

test/unit/SparkPostTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ public function setUp()
6060
$this->responseMock->shouldReceive('getBody')->andReturn($responseBodyMock);
6161
$responseBodyMock->shouldReceive('__toString')->andReturn(json_encode($this->responseBody));
6262

63+
$errorBodyMock = Mockery::mock();
64+
$this->badResponseBody = ['errors' => []];
65+
$this->badResponseMock = Mockery::mock('Psr\Http\Message\ResponseInterface');
66+
$this->badResponseMock->shouldReceive('getStatusCode')->andReturn(503);
67+
$this->badResponseMock->shouldReceive('getBody')->andReturn($errorBodyMock);
68+
$errorBodyMock->shouldReceive('__toString')->andReturn(json_encode($this->badResponseBody));
69+
6370
// exception mock up
6471
$exceptionResponseMock = Mockery::mock();
6572
$this->exceptionBody = ['results' => 'failed'];
@@ -159,6 +166,35 @@ public function testUnsuccessfulSyncRequest()
159166
}
160167
}
161168

169+
public function testSuccessfulSyncRequestWithRetries()
170+
{
171+
$this->clientMock->shouldReceive('sendRequest')->
172+
with(Mockery::type('GuzzleHttp\Psr7\Request'))->
173+
andReturn($this->badResponseMock, $this->badResponseMock, $this->responseMock);
174+
175+
$this->resource->setOptions(['retries' => 2]);
176+
$response = $this->resource->syncRequest('POST', 'transmissions', $this->postTransmissionPayload);
177+
178+
$this->assertEquals($this->responseBody, $response->getBody());
179+
$this->assertEquals(200, $response->getStatusCode());
180+
}
181+
182+
public function testUnsuccessfulSyncRequestWithRetries()
183+
{
184+
$this->clientMock->shouldReceive('sendRequest')->
185+
once()->
186+
with(Mockery::type('GuzzleHttp\Psr7\Request'))->
187+
andThrow($this->exceptionMock);
188+
189+
$this->resource->setOptions(['retries' => 2]);
190+
try {
191+
$this->resource->syncRequest('POST', 'transmissions', $this->postTransmissionPayload);
192+
} catch (\Exception $e) {
193+
$this->assertEquals($this->exceptionBody, $e->getBody());
194+
$this->assertEquals(500, $e->getCode());
195+
}
196+
}
197+
162198
public function testSuccessfulAsyncRequestWithWait()
163199
{
164200
$this->promiseMock->shouldReceive('wait')->andReturn($this->responseMock);
@@ -212,6 +248,46 @@ public function testUnsuccessfulAsyncRequestWithThen()
212248
})->wait();
213249
}
214250

251+
public function testSuccessfulAsyncRequestWithRetries()
252+
{
253+
$testReq = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []);
254+
$clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client');
255+
$clientMock->shouldReceive('sendAsyncRequest')->
256+
with(Mockery::type('GuzzleHttp\Psr7\Request'))->
257+
andReturn(
258+
new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->badResponseMock), $testReq),
259+
new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->badResponseMock), $testReq),
260+
new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->responseMock), $testReq)
261+
);
262+
263+
$resource = new SparkPost($clientMock, ['key' => 'SPARKPOST_API_KEY']);
264+
265+
$resource->setOptions(['async' => true, 'retries' => 2]);
266+
$promise = $resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload);
267+
$promise->then(function($resp) {
268+
$this->assertEquals(200, $resp->getStatusCode());
269+
})->wait();
270+
}
271+
272+
public function testUnsuccessfulAsyncRequestWithRetries()
273+
{
274+
$testReq = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []);
275+
$rejectedPromise = new GuzzleRejectedPromise($this->exceptionMock);
276+
$clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client');
277+
$clientMock->shouldReceive('sendAsyncRequest')->
278+
with(Mockery::type('GuzzleHttp\Psr7\Request'))->
279+
andReturn(new GuzzleAdapterPromise($rejectedPromise, $testReq));
280+
281+
$resource = new SparkPost($clientMock, ['key' => 'SPARKPOST_API_KEY']);
282+
283+
$resource->setOptions(['async' => true, 'retries' => 2]);
284+
$promise = $resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload);
285+
$promise->then(null, function($exception) {
286+
$this->assertEquals(500, $exception->getCode());
287+
$this->assertEquals($this->exceptionBody, $exception->getBody());
288+
})->wait();
289+
}
290+
215291
public function testPromise()
216292
{
217293
$promise = $this->resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload);

0 commit comments

Comments
 (0)