From 7327bf7bab1b8cd741263271e56dea04fce84151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hr=C3=A1=C5=A1ek?= Date: Wed, 18 Sep 2019 15:03:35 +0200 Subject: [PATCH 1/2] Add silent reply request handler --- src/ApiModule.php | 116 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 8 deletions(-) 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; + }); + } } From 9ac497ed0c5bd2fea38fafd13c6829f8ad4a458b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hr=C3=A1=C5=A1ek?= Date: Wed, 18 Sep 2019 15:28:17 +0200 Subject: [PATCH 2/2] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) 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.