diff --git a/README.md b/README.md index 818f768..2e7a446 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/ApiModule.php b/src/ApiModule.php index b508404..85939fe 100644 --- a/src/ApiModule.php +++ b/src/ApiModule.php @@ -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; @@ -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} */ @@ -40,15 +51,8 @@ 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; } @@ -56,6 +60,9 @@ public function bootstrap($app) $app->set('errorHandler', $errorHandler); $errorHandler->register(); + /** @var \yii\web\Request $request */ + $request = $app->getRequest(); + // disable csrf cookie $request->enableCsrfCookie = false; @@ -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); @@ -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]]. @@ -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; + }); + } }