Skip to content

Commit

Permalink
Follow redirects (#11)
Browse files Browse the repository at this point in the history
* Follow redirects

* Rename retry request

* Remove retryable interface

* Refactor and update doc
  • Loading branch information
jenky authored Jun 16, 2023
1 parent ccd542a commit bb4cd54
Show file tree
Hide file tree
Showing 20 changed files with 475 additions and 84 deletions.
4 changes: 4 additions & 0 deletions docs/advanced/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ Middleware provide a convenient mechanism for inspecting and modifying HTTP requ

Additional middleware can be written to perform a variety of tasks. For example, a logging middleware might log all outgoing requests and responses.

!!!
Middleware is **mutable**. If you want to apply middleware to only one request, use `clone` to avoid mutating the connector middleware.
!!!

## Defining Middleware

To create a new middleware, create a new Invokable class and put your logic inside `__invoke` method:
Expand Down
111 changes: 75 additions & 36 deletions docs/advanced/retrying-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,92 +2,131 @@
label: Retrying Requests
---

Sometimes you may deal with APIs that fail frequently because of network issues or temporary server errors. You may use the `retry` method to send a request and retry multiple times.
Sometimes you may deal with APIs that fail frequently because of network issues or temporary server errors. You may use the `Jenky\Atlas\RetryableConnector` decorator to send a request and retry multiple times.

## Getting Started

In order to retry a failed requests, your connector must implements `Jenky\Atlas\Contracts\RetryableConnectorInterface` interface instead of `Jenky\Atlas\Contracts\ConnectorInterface` and additionally add `Jenky\Atlas\Traits\Retryable` trait to the connector to fullfil the contract interface. The `retry` method accepts the maximum number of times the request should be attempted and a retry strategy to decide if the request should be retried, and to define the waiting time between each retry.
To retry a failed request, you should wrap you connector inside `Jenky\Atlas\RetryableConnector`. The connector accepts the maximum number of times the request should be attempted, a retry strategy to decide if the request should be retried, and to define the waiting time between each retry.

+++ Definition
```php
<?php
use Jenky\Atlas\RetryableConnector;

use Jenky\Atlas\Contracts\RetryableConnectorInterface;
use Jenky\Atlas\Traits\ConnectorTrait;
use Jenky\Atlas\Traits\Retryable;

final class MyConnector implements RetryableConnectorInterface
{
use ConnectorTrait;
use Retryable;
}
```
+++ Usage
```php
$connector = new MyConnector();
$connector = new RetryableConnector(new MyConnector());
$response = $connector->send(new MyRequest());

$response = $connector->retry()->send(new MyRequest());
// or retries for 5 times
$response = $connector->retry(5)->send(new MyRequest());

$connector = new RetryableConnector(new MyConnector(), 5);
$response = $connector->send(new MyRequest());
```
+++

## Customising When a Retry Is Attempted

By default, failed requests are retried up to 3 times, with an exponential delay between retries (first retry = 1 second; second retry: 2 seconds, third retry: 4 seconds) and only for the following HTTP status codes: `423`, `425`, `429`, `502` and `503` when using any HTTP method and `500`, `504`, `507` and `510` when using an HTTP [idempotent method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods).

If needed, you may pass a second argument to the `retry` method. The second argument is an instance of `Jenky\Atlas\Contracts\RetryStrategyInterface` that determines if the retries should actually be attempted. This will retries the failed requests with a delay of 1 second.
If needed, you may pass a third argument to the `Jenky\Atlas\RetryableConnector` instance. It is an instance of `Jenky\Atlas\Contracts\RetryStrategyInterface` that determines if the retries should actually be attempted. This will retries the failed requests with a delay of 1 second.

```php
use Jenky\Atlas\RetryableConnector;
use Jenky\Atlas\Retry\RetryCallback;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

$connector->retry(3, RetryCallback::when(static function (RequestInterface $request, ResponseInterface $response) {
return $response->getStatusCode() >= 500;
}))->send(new MyRequest());
$connector = new RetryableConnector(
new MyConnector(),
3,
RetryCallback::when(static function (RequestInterface $request, ResponseInterface $response) {
return $response->getStatusCode() >= 500;
})
)->send(new MyRequest());
```

### Customising Delay

You may also pass second and third arguments to the `RetryCallback::when()` method to customise the waiting time between each retry.

```php
use Jenky\Atlas\RetryableConnector;
use Jenky\Atlas\Retry\RetryCallback;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

$connector->retry(3, RetryCallback::when(static function (RequestInterface $request, ResponseInterface $response) {
// Your logic here
}, delay: 1000, multiplier: 2.0))->send(new MyRequest());
$connector = new RetryableConnector(
new MyConnector(),
3,
RetryCallback::when(static function (RequestInterface $request, ResponseInterface $response) {
// Your logic here
}, delay: 1000, multiplier: 2.0)
)->send(new MyRequest());
```

In the example above, failed requests are retried up to 3 times, with an exponential delay between retries (first retry = 1 second; second retry: 2 seconds, third retry: 4 seconds).

Instead of using an interval delay or calculated exponential delay, you may easily configure "exponential" backoffs by using `withDelay()` method. In this example, the retry delay will be 1 second for the first retry, 3 seconds for the second retry, and 10 seconds for the third retry:

```php
use Jenky\Atlas\RetryableConnector;
use Jenky\Atlas\Retry\Backoff;
use Jenky\Atlas\Retry\RetryCallback;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

$connector->retry(3, RetryCallback::when(static function (RequestInterface $request, ResponseInterface $response) {
// Your logic here
})->withDelay(new Backoff([1, 3, 10])))->send(new MyRequest());
$connector = new RetryableConnector(
new MyConnector(),
3,
RetryCallback::when(static function (RequestInterface $request, ResponseInterface $response) {
// Your logic here
})->withDelay(new Backoff([1, 3, 10]))
)->send(new MyRequest());
```

!!! Default retry strategy
As a SDK developer, you may set the default retry strategy by defining `defaultRetryStrategy()` method in the connector class.
!!!

## Disabling Throwing Exceptions

If a request fails, it will be attempted again - if it reaches the maximum number of errors, a `RequestRetryFailedException` will be thrown. If a request is successful at any point, it will return a `Response` instance.
If a request fails, it will be attempted again - if it reaches the maximum number of errors, a `Jenky\Atlas\Exception\RequestRetryFailedException` will be thrown. If a request is successful at any point, it will return a `Jenky\Atlas\Response` instance.

If you would like to disable this behavior, you may provide a `throw` argument with a value of `false`. When disabled, the last response received by the client will be returned after all retries have been attempted:


```php
$connector->retry(3, null, throw: false)->send(new MyRequest());
$connector = new RetryableConnector(new MyConnector(), 3, null, throw: false);
$response = $connector->send(new MyRequest());
```

## Retrying All Requests

Since middleware is mutable, adding new middleware means that all subsequent requests will also have it applied.

+++ Definition
```php
<?php

use Jenky\Atlas\Contracts\ConnectorInterface;
use Jenky\Atlas\Traits\ConnectorTrait;

final class MyConnector implements ConnectorInterface
{
use ConnectorTrait;
}
```
+++ Usage
```php
use Jenky\Atlas\Middleware\RetryRequests;
use Jenky\Atlas\Retry\Delay;
use Jenky\Atlas\Retry\GenericRetryStrategy;

$connector = new MyConnector();

$connector->middleware()->unshift(new RetryRequests(
new GenericRetryStrategy(new Delay(1000, 2.0)),
));
$response = $connector->send(new MyRequest());

// or always retries for 5 times

$connector->middleware()->unshift(new RetryRequests(
new GenericRetryStrategy(new Delay(1000, 2.0)),
5
));
$response = $connector->send(new MyRequest());
```
+++
2 changes: 1 addition & 1 deletion src/Contracts/ConnectorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function client(): ClientInterface;
/**
* Get the middleware instance.
*/
// public function middleware(): Middleware;
public function middleware(): Middleware;

/**
* Send the given request.
Expand Down
17 changes: 0 additions & 17 deletions src/Contracts/RetryableConnectorInterface.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

namespace Jenky\Atlas\Exception;

class ClientException extends HttpException
class ClientRequestException extends HttpException
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

namespace Jenky\Atlas\Exception;

class ServerException extends HttpException
class ServerRequestException extends HttpException
{
}
9 changes: 9 additions & 0 deletions src/Exception/TooManyRedirectsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Jenky\Atlas\Exception;

class TooManyRedirectsException extends \Exception
{
}
31 changes: 31 additions & 0 deletions src/FollowRedirectsConnector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Jenky\Atlas;

use Jenky\Atlas\Contracts\ConnectorInterface;
use Jenky\Atlas\Middleware\FollowRedirects;
use Jenky\Atlas\Traits\ConnectorDecoratorTrait;

class FollowRedirectsConnector implements ConnectorInterface
{
use ConnectorDecoratorTrait;

public function __construct(
ConnectorInterface $connector,
int $max = 5,
array $protocols = ['http', 'https'],
bool $strict = false,
bool $referer = false
) {
$clone = clone $connector;

$clone->middleware()
->unshift(new FollowRedirects(
$max, $protocols, $strict, $referer
));

$this->connector = $clone;
}
}
Loading

0 comments on commit bb4cd54

Please sign in to comment.