attributes->get('preview', false) || ! $request->isMethodCacheable() || $response->getStatusCode() >= 400) { $response->headers->set('Cache-Control', 'no-store, private'); return $response; } $response->headers->set( 'Cache-Control', "public, max-age=0, s-maxage={$seconds}, stale-while-revalidate=".($seconds * 10), ); // Responses vary by language, and the language is negotiated from the // header as well as the query string. Without this a shared cache // would serve Catalan to a Spanish visitor. $response->headers->set('Vary', 'Accept-Language, Accept, Origin'); $this->applyEntityTag($request, $response); return $response; } /** * A content-derived ETag, so an unchanged payload costs the caller a 304 * with an empty body instead of the full JSON. */ protected function applyEntityTag(Request $request, Response $response): void { if (! $response->getContent()) { return; } $etag = '"'.hash('xxh128', $response->getContent()).'"'; $response->headers->set('ETag', $etag); $ifNoneMatch = $request->headers->get('If-None-Match'); if ($ifNoneMatch !== null && $this->matches($ifNoneMatch, $etag)) { $response->setNotModified(); } } /** * `If-None-Match` may carry several tags, and a proxy may have weakened * ours with a `W/` prefix along the way. */ protected function matches(string $ifNoneMatch, string $etag): bool { foreach (explode(',', $ifNoneMatch) as $candidate) { $candidate = ltrim(trim($candidate), 'W/'); if ($candidate === '*' || hash_equals($etag, $candidate)) { return true; } } return false; } }