Skip to content

Commit cd4978b

Browse files
authored
Surface API error messages and standardize HTTP status fallback messages (#89)
1 parent f1edd47 commit cd4978b

9 files changed

Lines changed: 244 additions & 84 deletions

File tree

examples/USEnrichmentEtagExample.php

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
use SmartyStreets\PhpSdk\BasicAuthCredentials;
1010
use SmartyStreets\PhpSdk\ClientBuilder;
11-
use SmartyStreets\PhpSdk\Exceptions\RequestNotModifiedException;
1211
use SmartyStreets\PhpSdk\US_Enrichment\Business\Detail\Lookup as DetailLookup;
1312
use SmartyStreets\PhpSdk\US_Enrichment\Business\Summary\Lookup as SummaryLookup;
1413

@@ -40,10 +39,12 @@ function exerciseSummaryEtag($client, string $smartyKey): ?string {
4039
$second->setRequestEtag($captured);
4140
try {
4241
$client->sendBusinessLookup($second);
43-
echo " Call 2 (matching Etag): 200 — server did NOT honor the conditional. Results=" . count($second->getResults())
44-
. ", Etag=" . display($second->getResponseEtag()) . "\n";
45-
} catch (RequestNotModifiedException $ex) {
46-
echo " Call 2 (matching Etag): 304 RequestNotModifiedException — caller treats this as cache-valid. Refreshed Etag=" . display($ex->getResponseEtag()) . "\n";
42+
if ($second->getResults() === null) {
43+
echo " Call 2 (matching Etag): 304 not modified — cache is still valid. Refreshed Etag=" . display($second->getResponseEtag()) . "\n";
44+
} else {
45+
echo " Call 2 (matching Etag): 200 — server did NOT honor the conditional. Results=" . count($second->getResults())
46+
. ", Etag=" . display($second->getResponseEtag()) . "\n";
47+
}
4748
} catch (Exception $ex) {
4849
echo " Call 2 unexpected failure: " . get_class($ex) . ": " . $ex->getMessage() . "\n";
4950
return null;
@@ -53,10 +54,12 @@ function exerciseSummaryEtag($client, string $smartyKey): ?string {
5354
$third->setRequestEtag($captured . "X");
5455
try {
5556
$client->sendBusinessLookup($third);
56-
echo " Call 3 (mutated Etag): 200 as expected. Results=" . count($third->getResults())
57-
. ", Etag=" . display($third->getResponseEtag()) . "\n";
58-
} catch (RequestNotModifiedException $ex) {
59-
echo " Call 3 (mutated Etag): 304 — UNEXPECTED. Server treated a different Etag as matching.\n";
57+
if ($third->getResults() === null) {
58+
echo " Call 3 (mutated Etag): 304 — UNEXPECTED. Server treated a different Etag as matching.\n";
59+
} else {
60+
echo " Call 3 (mutated Etag): 200 as expected. Results=" . count($third->getResults())
61+
. ", Etag=" . display($third->getResponseEtag()) . "\n";
62+
}
6063
} catch (Exception $ex) {
6164
echo " Call 3 unexpected failure: " . get_class($ex) . ": " . $ex->getMessage() . "\n";
6265
}
@@ -89,11 +92,13 @@ function exerciseDetailEtag($client, string $businessId): void {
8992
$second->setRequestEtag($captured);
9093
try {
9194
$client->sendBusinessDetailLookup($second);
92-
echo " Call 2 (matching Etag): 200 — server did NOT honor the conditional. businessId="
93-
. ($second->getResult()->businessId ?? '<null>') . ", Etag=" . display($second->getResponseEtag()) . "\n";
94-
} catch (RequestNotModifiedException $ex) {
95-
echo " Call 2 (matching Etag): 304 RequestNotModifiedException — caller treats this as cache-valid. Refreshed Etag="
96-
. display($ex->getResponseEtag()) . "\n";
95+
if ($second->getResult() === null) {
96+
echo " Call 2 (matching Etag): 304 not modified — cache is still valid. Refreshed Etag="
97+
. display($second->getResponseEtag()) . "\n";
98+
} else {
99+
echo " Call 2 (matching Etag): 200 — server did NOT honor the conditional. businessId="
100+
. ($second->getResult()->businessId ?? '<null>') . ", Etag=" . display($second->getResponseEtag()) . "\n";
101+
}
97102
} catch (Exception $ex) {
98103
echo " Call 2 unexpected failure: " . get_class($ex) . ": " . $ex->getMessage() . "\n";
99104
return;
@@ -103,10 +108,12 @@ function exerciseDetailEtag($client, string $businessId): void {
103108
$third->setRequestEtag($captured . "X");
104109
try {
105110
$client->sendBusinessDetailLookup($third);
106-
echo " Call 3 (mutated Etag): 200 as expected. businessId="
107-
. ($third->getResult()->businessId ?? '<null>') . ", Etag=" . display($third->getResponseEtag()) . "\n";
108-
} catch (RequestNotModifiedException $ex) {
109-
echo " Call 3 (mutated Etag): 304 — UNEXPECTED. Server treated a different Etag as matching.\n";
111+
if ($third->getResult() === null) {
112+
echo " Call 3 (mutated Etag): 304 — UNEXPECTED. Server treated a different Etag as matching.\n";
113+
} else {
114+
echo " Call 3 (mutated Etag): 200 as expected. businessId="
115+
. ($third->getResult()->businessId ?? '<null>') . ", Etag=" . display($third->getResponseEtag()) . "\n";
116+
}
110117
} catch (Exception $ex) {
111118
echo " Call 3 unexpected failure: " . get_class($ex) . ": " . $ex->getMessage() . "\n";
112119
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace SmartyStreets\PhpSdk\Exceptions;
4+
require_once 'SmartyException.php';
5+
6+
class ForbiddenException extends SmartyException {
7+
8+
}

src/HeaderUtil.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public static function extractEtag($headers): ?string {
1414
}
1515
foreach ($headers as $key => $value) {
1616
if (is_string($key) && strcasecmp($key, 'etag') === 0) {
17-
return is_array($value) ? ($value[0] ?? null) : $value;
17+
$etag = is_array($value) ? ($value[0] ?? null) : $value;
18+
return $etag === null ? null : trim($etag);
1819
}
1920
}
2021
return null;

src/StatusCodeSender.php

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
include_once('Sender.php');
66
require_once(__DIR__ . '/HeaderUtil.php');
77
require_once(__DIR__ . '/Exceptions/BadCredentialsException.php');
8+
require_once(__DIR__ . '/Exceptions/BadGatewayException.php');
89
require_once(__DIR__ . '/Exceptions/BadRequestException.php');
10+
require_once(__DIR__ . '/Exceptions/ForbiddenException.php');
911
require_once(__DIR__ . '/Exceptions/InternalServerErrorException.php');
1012
require_once(__DIR__ . '/Exceptions/PaymentRequiredException.php');
1113
require_once(__DIR__ . '/Exceptions/RequestEntityTooLargeException.php');
1214
require_once(__DIR__ . '/Exceptions/RequestNotModifiedException.php');
15+
require_once(__DIR__ . '/Exceptions/RequestTimeoutException.php');
1316
require_once(__DIR__ . '/Exceptions/ServiceUnavailableException.php');
1417
require_once(__DIR__ . '/Exceptions/TooManyRequestsException.php');
1518
require_once(__DIR__ . '/Exceptions/UnprocessableEntityException.php');
@@ -18,6 +21,7 @@
1821
use SmartyStreets\PhpSdk\Exceptions\BadCredentialsException;
1922
use SmartyStreets\PhpSdk\Exceptions\BadGatewayException;
2023
use SmartyStreets\PhpSdk\Exceptions\BadRequestException;
24+
use SmartyStreets\PhpSdk\Exceptions\ForbiddenException;
2125
use SmartyStreets\PhpSdk\Exceptions\InternalServerErrorException;
2226
use SmartyStreets\PhpSdk\Exceptions\PaymentRequiredException;
2327
use SmartyStreets\PhpSdk\Exceptions\RequestEntityTooLargeException;
@@ -44,24 +48,25 @@ public function __construct(Sender $inner)
4448
function messageFrom(Response $response, String $fallback)
4549
{
4650
$payload = $response->getPayload();
47-
if ($payload === null || $payload === '') {
48-
return $fallback;
49-
}
51+
$body = $payload === null ? '' : trim($payload);
5052

51-
$responseJSON = json_decode($payload, true, 10);
53+
if ($body !== '') {
54+
$responseJSON = json_decode($payload, true, 10);
5255

53-
if (! isset($responseJSON['errors'])) {
54-
return $fallback;
55-
}
56+
if (isset($responseJSON['errors'])) {
57+
$errorMessage = '';
58+
foreach ($responseJSON['errors'] as $error) {
59+
$errorMessage .= isset($error['message']) ? $error['message'] . ' ' : '';
60+
}
5661

57-
$errorMessage = '';
58-
foreach ($responseJSON['errors'] as $error) {
59-
$errorMessage .= isset($error['message']) ? $error['message'] . ' ' : '';
62+
$errorMessage = trim($errorMessage);
63+
if ($errorMessage !== '') {
64+
return $errorMessage;
65+
}
66+
}
6067
}
6168

62-
$errorMessage = trim($errorMessage);
63-
64-
return $errorMessage === '' ? $fallback : $errorMessage;
69+
return trim($fallback . ' Body: ' . $body);
6570
}
6671

6772
function send(Request $request)
@@ -70,48 +75,39 @@ function send(Request $request)
7075

7176
switch ($response->getStatusCode()) {
7277
case 200:
73-
return $response;
7478
case 304:
75-
throw new RequestNotModifiedException("Record has not been modified since the last request.", $response->getStatusCode(), null, HeaderUtil::extractEtag($response->getHeaders()));
79+
return $response;
7680
case 400:
77-
throw new BadRequestException($this->messageFrom($response, "Bad Request (Malformed Payload): A GET request lacked a street field or the request body of a POST request contained malformed JSON."), $response->getStatusCode());
81+
throw new BadRequestException($this->messageFrom($response, "Bad Request (Malformed Payload): A GET request lacked a required field or the request body of a POST request contained malformed JSON."), $response->getStatusCode());
7882
case 401:
7983
throw new BadCredentialsException($this->messageFrom($response, "Unauthorized: The credentials were provided incorrectly or did not match any existing, active credentials."), $response->getStatusCode());
8084
case 402:
8185
throw new PaymentRequiredException($this->messageFrom($response, "Payment Required: There is no active subscription for the account associated with the credentials submitted with the request."), $response->getStatusCode());
86+
case 403:
87+
throw new ForbiddenException($this->messageFrom($response, "Forbidden: The request contained valid data and was understood by the server, but the server is refusing action."), $response->getStatusCode());
8288
case 408:
8389
throw new RequestTimeoutException($this->messageFrom($response, "Request timeout error."), $response->getStatusCode());
8490
case 413:
8591
throw new RequestEntityTooLargeException($this->messageFrom($response, "Request Entity Too Large: The request body has exceeded the maximum size."), $response->getStatusCode());
8692
case 422:
8793
throw new UnprocessableEntityException($this->messageFrom($response, "GET request lacked required fields."), $response->getStatusCode());
8894
case 429:
89-
$responseJSON = json_decode($response->getPayload(), true, 10);
90-
9195
$retryAfterValue = DEFAULT_BACKOFF_DURATION;
9296
if (isset($response->getHeaders()['retry-after'])){
9397
$retryAfterValue = intval($response->getHeaders()['retry-after']);
9498
}
9599

96-
if (! isset($responseJSON['errors'])) {
97-
throw new TooManyRequestsException("The rate limit for the plan associated with this subscription has been exceeded. To see plans with higher rate limits, visit our pricing page.", $response->getStatusCode(), $retryAfterValue);
98-
}
99-
$errorMessage = '';
100-
foreach($responseJSON['errors'] as $error){
101-
$errorMessage .= isset($error['message']) ? $error['message'].' ': '';
102-
}
103-
104-
throw new TooManyRequestsException($errorMessage, $response->getStatusCode(), $retryAfterValue);
100+
throw new TooManyRequestsException($this->messageFrom($response, "Too Many Requests: The rate limit for your account has been exceeded."), $response->getStatusCode(), $retryAfterValue);
105101
case 500:
106-
throw new InternalServerErrorException("Internal Server Error.", $response->getStatusCode());
102+
throw new InternalServerErrorException($this->messageFrom($response, "Internal Server Error."), $response->getStatusCode());
107103
case 502:
108-
throw new BadGatewayException("Bad Gateway error.", $response->getStatusCode());
104+
throw new BadGatewayException($this->messageFrom($response, "Bad Gateway error."), $response->getStatusCode());
109105
case 503:
110-
throw new ServiceUnavailableException("Service Unavailable. Try again later.", $response->getStatusCode());
106+
throw new ServiceUnavailableException($this->messageFrom($response, "Service Unavailable. Try again later."), $response->getStatusCode());
111107
case 504:
112-
throw new GatewayTimeoutException("The upstream data provider did not respond in a timely fashion and the request failed. A serious, yet rare occurrence indeed.", $response->getStatusCode());
108+
throw new GatewayTimeoutException($this->messageFrom($response, "The upstream data provider did not respond in a timely fashion and the request failed. A serious, yet rare occurrence indeed."), $response->getStatusCode());
113109
default:
114-
throw new SmartyException("Error sending request. Status code is: ", $response->getStatusCode());
110+
throw new SmartyException($this->messageFrom($response, "The server returned an unexpected HTTP status code: " . $response->getStatusCode()), $response->getStatusCode());
115111
}
116112
}
117113
}

src/US_Enrichment/Business/Summary/Lookup.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@
88
use SmartyStreets\PhpSdk\US_Enrichment\Lookup as EnrichmentLookup;
99

1010
class Lookup extends EnrichmentLookup {
11-
/** @var Result[] */
12-
private array $results = [];
11+
/** @var Result[]|null */
12+
private ?array $results = null;
1313

1414
public function __construct(?string $smartyKey = null) {
1515
parent::__construct($smartyKey, 'business', null);
1616
}
1717

1818
/**
19-
* @return Result[]
19+
* @return Result[]|null Null when the record was not modified (304) or no request has been sent.
2020
*/
21-
public function getResults(): array {
21+
public function getResults(): ?array {
2222
return $this->results;
2323
}
2424

src/US_Enrichment/Client.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function sendGenericLookup($genericLookup, string $dataSetName, ?string $
5555
* @param BusinessSummaryLookup|string|null $businessLookup SmartyKey string or a Business\Summary\Lookup.
5656
* @return \SmartyStreets\PhpSdk\US_Enrichment\Business\Summary\Result[]
5757
*/
58-
public function sendBusinessLookup($businessLookup): array {
58+
public function sendBusinessLookup($businessLookup): ?array {
5959
if (is_string($businessLookup)) {
6060
$lookup = new BusinessSummaryLookup($businessLookup);
6161
} elseif ($businessLookup instanceof BusinessSummaryLookup) {
@@ -127,6 +127,9 @@ private function dispatch(Request $request, EnrichmentLookupBase $lookup): void
127127
if ($etag !== null) {
128128
$lookup->setResponseEtag($etag);
129129
}
130+
if ($response->getStatusCode() === 304) {
131+
return;
132+
}
130133
$payload = $response->getPayload();
131134
$decoded = $payload === null || $payload === '' ? null : $this->serializer->deserialize($payload);
132135
$lookup->buildResults(is_array($decoded) ? $decoded : null);

0 commit comments

Comments
 (0)