Skip to content

Commit

Permalink
feat: Add option to configure HTTP status codes to consider as failure
Browse files Browse the repository at this point in the history
closes #73
  • Loading branch information
pvgnd committed Mar 25, 2021
1 parent aa4fa2b commit 627e516
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 9 deletions.
63 changes: 56 additions & 7 deletions src/Ganesha/GaneshaHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,54 @@ final class GaneshaHttpClient implements HttpClientInterface
*/
private $serviceNameExtractor;

/**
* @var array<string, mixed>
*/
private $defaultOptions = [
// 4xx and 5xx are considered as success by default because server responded
'ganesha.failure_status_codes' => [], // array - containing HTTP status codes in 4XX and 5XX ranges that
// should be considered as failure (int value expected)
];

/**
* @param array<string, mixed> $defaultOptions An array containing valid GaneshaHttpClient options
*/
public function __construct(
HttpClientInterface $client,
Ganesha $ganesha,
?ServiceNameExtractorInterface $serviceNameExtractor = null
?ServiceNameExtractorInterface $serviceNameExtractor = null,
array $defaultOptions = []
) {
$this->client = $client;
$this->ganesha = $ganesha;
$this->serviceNameExtractor = $serviceNameExtractor ?: new ServiceNameExtractor();
$this->defaultOptions = self::mergeDefaultOptions($this->defaultOptions, $defaultOptions);
}

/**
* @param array<string, mixed> $defaultOptions
* @param array<string, mixed> $options
* @return array<string, mixed>
*/
private static function mergeDefaultOptions(array $defaultOptions, array $options): array
{
return \array_merge($defaultOptions, $options);
}

/**
* @param array<string, mixed> $options
* @return array<string, mixed>
*/
private function avoidGaneshaOptionsPropagation(array $options): array
{
$optionsToUnset = array_keys($this->defaultOptions);
$optionsToUnset[] = ServiceNameExtractor::OPTION_KEY;

foreach ($optionsToUnset as $optionName) {
unset($options[$optionName]);
}

return $options;
}

/**
Expand All @@ -47,23 +87,24 @@ public function __construct(
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$options = self::mergeDefaultOptions($this->defaultOptions, $options);
$serviceName = $this->serviceNameExtractor->extract($method, $url, $options);

if (!$this->ganesha->isAvailable($serviceName)) {
throw new RejectedException(sprintf('"%s" is not available', $serviceName));
}

// Do not propagate option unsupported by decorated client instance
unset($options[ServiceNameExtractor::OPTION_KEY]);

$response = $this->client->request($method, $url, $options);
$response = $this->client->request($method, $url, $this->avoidGaneshaOptionsPropagation($options));
try {
$response->getHeaders();

$this->ganesha->success($serviceName);
} catch (ClientExceptionInterface | ServerExceptionInterface $e) {
// 4xx and 5xx are considered as success because server responded
$this->ganesha->success($serviceName);
if ($this->isFailureStatusCode($e->getResponse()->getStatusCode(), $options)) {
$this->ganesha->failure($serviceName);
} else {
$this->ganesha->success($serviceName);
}
} catch (RedirectionExceptionInterface | TransportExceptionInterface $e) {
// 3xx when max redirection is reached and network issues are considered as failure
$this->ganesha->failure($serviceName);
Expand All @@ -79,4 +120,12 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa
{
return $this->client->stream($responses, $timeout);
}

/**
* @param array<string, mixed> $options
*/
private function isFailureStatusCode(int $responseStatusCode, array $options): bool
{
return \in_array($responseStatusCode, $options['ganesha.failure_status_codes'], true);
}
}
43 changes: 41 additions & 2 deletions tests/Ackintosh/Ganesha/GaneshaHttpClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,44 @@ public function recordsSuccessOn500(): void
);
}

/**
* @test
*/
public function recordsFailureOnConfiguredHttpStatusCodeAtRequestLevel(): void
{
$httpResponse = new MockResponse('', [ 'http_code' => 503 ]);
$client = $this->buildClient(null, [$httpResponse]);

$client->request('GET', 'http://api.example.com/awesome_resource/503', [
'ganesha.failure_status_codes' => [503]
]);

self::assertSame(
1,
$this->adapter->load(
Storage\StorageKeys::KEY_PREFIX.'api.example.com'.Storage\StorageKeys::KEY_SUFFIX_FAILURE
)
);
}

/**
* @test
*/
public function recordsFailureOnConfiguredHttpStatusCodeAtClientLevel(): void
{
$httpResponse = new MockResponse('', [ 'http_code' => 503 ]);
$client = $this->buildClient(null, [$httpResponse], ['ganesha.failure_status_codes' => [503]]);

$client->request('GET', 'http://api.example.com/awesome_resource/503');

self::assertSame(
1,
$this->adapter->load(
Storage\StorageKeys::KEY_PREFIX.'api.example.com'.Storage\StorageKeys::KEY_SUFFIX_FAILURE
)
);
}

/**
* @test
*/
Expand Down Expand Up @@ -210,6 +248,7 @@ public function doNotPropagateGaneshaOptionToDecoratedInstance(): void
[
'max_duration' => 3.0,
Ganesha\HttpClient\ServiceNameExtractor::OPTION_KEY => 'an_awesome_service',
'ganesha.failure_status_codes' => [500,503]
]
);
}
Expand All @@ -236,11 +275,11 @@ public function streamDelegatesToDecoratedInstance(): void
/**
* @param MockResponse[] $responses
*/
private function buildClient(?Ganesha $ganesha = null, array $responses = []): HttpClientInterface
private function buildClient(?Ganesha $ganesha = null, array $responses = [], array $options = []): HttpClientInterface
{
$client = (0 === \count($responses)) ? HttpClient::create() : new MockHttpClient($responses);

return new GaneshaHttpClient($client, $ganesha ?? $this->buildGanesha());
return new GaneshaHttpClient($client, $ganesha ?? $this->buildGanesha(), null, $options);
}

private function buildGanesha(): Ganesha
Expand Down

0 comments on commit 627e516

Please sign in to comment.