Ticket 0054-FE section 5: (1) with the SSR flag on, /_frontend/assets/* is now proxied to the VPS release as well, deliberately without a file-exists guard so stale local SPA hashes cannot shadow the release; the proxy rules tag requests with env SSRPROXY and mod_headers adds X-Forwarded-Proto: https. (3) If-None-Match comparison in JsonCacheHeaders accepts the -gzip suffix that mod_deflate appends, so browser 304s work through compression. (4) X-TYPO3-Cache-Tags*/-Debug-Cache/-Cache-Lifetime/-Parsetime are stripped from responses (onsuccess and always tables - they leak record ids and overflowed the SSR node 16 KB header limit on news pages). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 lines
2.3 KiB
PHP
63 lines
2.3 KiB
PHP
<?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()) . '"';
|
|
|
|
// mod_deflate suffixes the ETag it compressed ("<md5>-gzip");
|
|
// browsers send that form back, so accept it too (0054-FE 5.3).
|
|
$ifNoneMatch = str_replace('-gzip"', '"', 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);
|
|
}
|
|
}
|