diff --git a/src/fetcher.php b/src/fetcher.php index 8975ab8..56d6918 100644 --- a/src/fetcher.php +++ b/src/fetcher.php @@ -179,14 +179,9 @@ private function serveFromCache(string $url, ?string $rawContent, ?callable $cal $merged = json_decode((string) $rawContent); - if (!$options->firstPageOnly) { + if (!$options->firstPageOnly && is_array($merged)) { $stmt = Workflow::getStatement('SELECT url, content FROM request_cache WHERE parent = ? ORDER BY `timestamp` DESC'); - $hasChildren = false; while ($stmt->execute([$url]) && $data = $stmt->fetchObject()) { - if (!$hasChildren) { - $merged = self::asList($merged); - $hasChildren = true; - } $merged = array_merge($merged, self::asList(json_decode($data->content))); $url = $data->url; } @@ -244,7 +239,8 @@ private function handleResponse(CurlResponse $response, ?string $cachedContent, Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, ?)') ->execute([$url, time(), $response->etag, json_encode($decoded), $parent]); - if ($options->firstPageOnly) { + if ($options->firstPageOnly || !is_array($decoded)) { + Workflow::getStatement('DELETE FROM request_cache WHERE parent = ?')->execute([$url]); $this->finish($rootUrl, $accumulator, $callback, $options); return; diff --git a/src/search.php b/src/search.php index 5a2de7d..87f2777 100644 --- a/src/search.php +++ b/src/search.php @@ -43,11 +43,14 @@ public static function run(string $scope, string $query, bool|string $hotkey): v return; } - if (!Workflow::getAccessToken() || !(self::$user = Fetcher::requestApi('/user'))) { + $user = Workflow::getAccessToken() ? Fetcher::requestApi('/user') : null; + if (!$user instanceof stdClass) { + Workflow::getStatement('DELETE FROM request_cache WHERE url = ?')->execute([Workflow::getApiUrl('/user')]); self::addLoginCommands(); return; } + self::$user = $user; Workflow::stopServer(); diff --git a/tests/FetcherTest.php b/tests/FetcherTest.php index 0571add..c4d5102 100644 --- a/tests/FetcherTest.php +++ b/tests/FetcherTest.php @@ -148,6 +148,39 @@ public function testRequestUrlReusesCachedContentOn304(): void self::assertSame(2, $this->totalHits()); } + public function testRequestUrlDoesNotMergeCorruptChildRowsIntoObjectResponse(): void + { + $url = self::baseUrl() . '/json?id=object'; + $childUrl = self::baseUrl() . '/json?id=child'; + + Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, NULL)') + ->execute([$url, time(), '"root"', json_encode(['id' => 'object', 'value' => 'val-object'])]); + Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, ?)') + ->execute([$childUrl, time(), '"child"', json_encode([['id' => 'child']]), $url]); + + $result = Fetcher::requestUrl($url); + + self::assertInstanceOf(stdClass::class, $result); + self::assertSame('object', $result->id); + } + + public function testRequestUrlDoesNotFollowCorruptCachedChildOnObject304(): void + { + $url = self::baseUrl() . '/etag?v=v1'; + $childUrl = self::baseUrl() . '/json?id=child'; + + Fetcher::requestUrl($url); + Workflow::getStatement('UPDATE request_cache SET timestamp = ? WHERE url = ?') + ->execute([time() - 3600, $url]); + Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, ?)') + ->execute([$childUrl, time(), '"child"', json_encode([['id' => 'child']]), $url]); + + $result = Fetcher::requestUrl($url, new FetchOptions(refreshInBackground: false)); + + self::assertInstanceOf(stdClass::class, $result); + self::assertSame('v1', $result->value); + } + public function testRequestUrlStaleWithBackgroundRefreshServesCacheAndMarksUrl(): void { $url = self::baseUrl() . '/json?id=stale';