JSON API: Cache-Control no-cache + ETag/304 revalidation
New JsonCacheHeaders middleware (outer, frontend stack): GET/HEAD 200 responses under /_json get a single Cache-Control: no-cache, Expires: 0 and a content md5 ETag; If-None-Match answers as empty 304. Logged-in visitors (fe_typo_user) keep TYPO3 headers. Expires: 0 is deliberate so mod_expires leaves the response alone. .htaccess: the global Header unset ETag now skips /_json (KEEP_ETAG env, carried across the mod_rewrite internal redirect as REDIRECT_KEEP_ETAG). Fulfils tickets 0011-FE point 3 and 0055-BE question 6; asset immutable headers were already live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dcd713f222
commit
351349ec1f
3 files changed
+79
-2
No files matched your search
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use TYPO3\CMS\Core\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes client caching for the headless JSON API (/_json):
|
||||||
|
* a single "Cache-Control: no-cache" plus a content ETag, answering
|
||||||
|
* If-None-Match with an empty 304 so browsers revalidate instead of
|
||||||
|
* re-downloading (tickets 0011-FE / 0055-BE).
|
||||||
|
*
|
||||||
|
* "Expires: 0" is set on purpose: mod_expires leaves responses alone
|
||||||
|
* that already carry an Expires header, otherwise it would re-add
|
||||||
|
* max-age/Expires on top of ours (public/.htaccess ExpiresByType).
|
||||||
|
*
|
||||||
|
* Logged-in visitors (fe_typo_user cookie) keep TYPO3's own headers:
|
||||||
|
* their responses may be personalized and must not be eligible for
|
||||||
|
* shared revalidation.
|
||||||
|
*/
|
||||||
|
final class JsonCacheHeaders implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
$path = $request->getUri()->getPath();
|
||||||
|
if (
|
||||||
|
!\in_array($request->getMethod(), ['GET', 'HEAD'], true)
|
||||||
|
|| $response->getStatusCode() !== 200
|
||||||
|
|| ($path !== '/_json' && !str_starts_with($path, '/_json/'))
|
||||||
|
|| isset($request->getCookieParams()['fe_typo_user'])
|
||||||
|
) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$etag = '"' . md5((string)$response->getBody()) . '"';
|
||||||
|
|
||||||
|
$ifNoneMatch = trim($request->getHeaderLine('If-None-Match'));
|
||||||
|
if ($ifNoneMatch !== '' && str_contains($ifNoneMatch, $etag)) {
|
||||||
|
return (new Response())
|
||||||
|
->withStatus(304)
|
||||||
|
->withHeader('ETag', $etag)
|
||||||
|
->withHeader('Cache-Control', 'no-cache')
|
||||||
|
->withHeader('Expires', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response
|
||||||
|
->withHeader('Cache-Control', 'no-cache')
|
||||||
|
->withoutHeader('Pragma')
|
||||||
|
->withHeader('Expires', '0')
|
||||||
|
->withHeader('ETag', $etag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,17 @@ return [
|
|||||||
'typo3/cms-core/normalized-params-attribute',
|
'typo3/cms-core/normalized-params-attribute',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
// Outer response filter: Cache-Control: no-cache + ETag/304 for
|
||||||
|
// /_json (tickets 0011-FE / 0055-BE).
|
||||||
|
'vitec/json-cache-headers' => [
|
||||||
|
'target' => \Evomedien\Vitec\Middleware\JsonCacheHeaders::class,
|
||||||
|
'after' => [
|
||||||
|
'typo3/cms-core/normalized-params-attribute',
|
||||||
|
],
|
||||||
|
'before' => [
|
||||||
|
'vitec/json-prefix-rewrite',
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'backend' => [
|
'backend' => [
|
||||||
'vitec/sync-ssr-flag' => [
|
'vitec/sync-ssr-flag' => [
|
||||||
|
|||||||
+8
-2
@@ -439,9 +439,15 @@ Options -MultiViews
|
|||||||
Header set X-Content-Type-Options "nosniff"
|
Header set X-Content-Type-Options "nosniff"
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
# ETag removal
|
# ETag removal - except for the /_json API: there TYPO3 (JsonCacheHeaders
|
||||||
|
# middleware) sends a content ETag so browsers can revalidate with 304
|
||||||
|
# (tickets 0011-FE / 0055-BE).
|
||||||
<IfModule mod_headers.c>
|
<IfModule mod_headers.c>
|
||||||
Header unset ETag
|
SetEnvIf Request_URI "^/_json($|/)" KEEP_ETAG=1
|
||||||
|
# After the mod_rewrite internal redirect to index.php the variable
|
||||||
|
# is renamed REDIRECT_KEEP_ETAG - carry it back over:
|
||||||
|
SetEnvIf REDIRECT_KEEP_ETAG 1 KEEP_ETAG=1
|
||||||
|
Header unset ETag env=!KEEP_ETAG
|
||||||
</IfModule>
|
</IfModule>
|
||||||
FileETag None
|
FileETag None
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user