Skip to content

Handling multiple requests with same Request ID #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ See [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to find l
If no `Accept-Language` header is found or language is not supported, first language in acceptLanguages array will be used as default. If no acceptLanguages is defined, is used base app language.

TODO make generating of language list in `acceptLanguages` param automatic, not user defined.

## Multiple requests with same ID

When you need better quality API you can send multiple requests to the same endpoint. You just need to send `X-Request-Id` header and all requests with the same request ID within 30 sec will return the same result as if you send one request.
116 changes: 108 additions & 8 deletions src/ApiModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Yii;
use yii\base\BootstrapInterface;
use yii\base\Module;
use yii\base\ExitException;
use yii\web\Application;
use yii\web\Response;
use yii\filters\ContentNegotiator;
Expand All @@ -31,6 +32,16 @@ class ApiModule extends Module implements BootstrapInterface

public $errorHandler = ErrorHandler::class;

/**
* @var string
*/
public $requestIdHeader = 'X-Request-Id';

/**
* @var bool
*/
private $_isValidRequest;

/**
* {@inheritdoc}
*/
Expand All @@ -40,22 +51,18 @@ public function bootstrap($app)
/** @var Application $app */
$app = $event->sender;

/** @var \yii\web\Request $request */
$request = $app->getRequest();

list($route, ) = $app->get('urlManager')->parseRequest($request);

$id = $this->getUniqueId();

// change app behavior only for requests to this module
if (\yii\helpers\StringHelper::startsWith($route, $id) === false) {
if (!$this->isValidRequest($app)) {
return;
}

$errorHandler = Yii::createObject($this->errorHandler);
$app->set('errorHandler', $errorHandler);
$errorHandler->register();

/** @var \yii\web\Request $request */
$request = $app->getRequest();

// disable csrf cookie
$request->enableCsrfCookie = false;

Expand All @@ -78,6 +85,8 @@ public function bootstrap($app)
});
});

$this->handleMultiRequest($app);

foreach ($this->getModules() as $name => $module) {
if(in_array(BootstrapInterface::class, class_implements($module))){
$this->getModule($name)->bootstrap($app);
Expand Down Expand Up @@ -111,6 +120,27 @@ public function afterAction($action, $result)
return $this->serializeData($result);
}

/**
* Validation for request event.
* @param Application $app
* @return boolean
*/
public function isValidRequest(Application $app): bool
{
if ($this->_isValidRequest === null) {
/** @var \yii\web\Request $request */
$request = $app->getRequest();

list($route, ) = $app->get('urlManager')->parseRequest($request);

$id = $this->getUniqueId();

// change app behavior only for requests to this module
$this->_isValidRequest = \yii\helpers\StringHelper::startsWith($route, $id);
}
return $this->_isValidRequest;
}

/**
* Serializes the specified data.
* The default implementation will create a serializer based on the configuration given by [[serializer]].
Expand All @@ -122,4 +152,74 @@ protected function serializeData($data)
{
return Yii::createObject($this->serializer)->serialize($data);
}

/**
* Handle OkHttp silent retry calls.
*
* @see https://medium.com/inloopx/okhttp-is-quietly-retrying-requests-is-your-api-ready-19489ef35ace
*
* @param Application $app
* @return void
*/
private function handleMultiRequest(Application $app)
{
/** @var yii\caching\CacheInterface $cache */
$cache = $app->getCache();

$app->on(Application::EVENT_BEFORE_REQUEST, function ($event) use ($cache) {
/** @var Application $app */
$app = $event->sender;

if (!$this->isValidRequest($app)) {
return;
}

$headers = $app->getRequest()->getHeaders();

if (!$headers->has($this->requestIdHeader)) {
return;
}

$this->requestId = $headers->get($this->requestIdHeader);

// when cached request exists, skip aplication request handling
if ($cache->exists($this->requestId)) {
throw new ExitException();
}

// allow response cache on after send event
$app->getResponse()->on(Response::EVENT_AFTER_SEND, function ($event) use ($cache) {
/** @var Response $response */
$response = $event->sender;

// don't cache stream response
if ($response->stream !== null) {
return;
}

$cacheResponse = new \stdClass();
$cacheResponse->headers = $response->getHeaders()->toArray();
$cacheResponse->cookies = $response->getCookies()->toArray();
$cacheResponse->statusCode = $response->getStatusCode();
$cacheResponse->statusText = $response->statusText;
$cacheResponse->content = $response->content;

$cache->set($this->requestId, $cacheResponse, 30);
});
});

$app->on(Application::EVENT_AFTER_REQUEST, function ($event) use ($cache) {
if (!$this->requestId || !$cache->exists($this->requestId)) {
return;
}

$response = $app->getResponse();
$cacheResponse = $cache->get($this->requestId);

$response->getHeaders()->fromArray($cacheResponse->headers);
$response->getCookies()->fromArray($cacheResponse->cookies);
$response->setStatusCode($cacheResponse->statusCode, $cacheResponse->statusText);
$response->content = $cacheResponse->content;
});
}
}