From 0bdf6daa7772354dcbeb9c4769a69242362a9c80 Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Sun, 3 Dec 2023 10:32:10 +0100
Subject: [PATCH] [FEATURE] FE cache information request attribute

Refactor TSFE->no_cache towards the new Request
attribute 'frontend.cache.instruction': This
attribute can be created by middlewares early
when they need to disable cache mechanics of
middlewares later in the stack.

The new construct *requires* a reason when caching
is disabled, and the disable instruction can be set
by multiple middlewares which can result in multiple
reasons being gathered. The admin panel now lists
any "cache has been disabled" reasons.

Resolves: #102628
Releases: main
Change-Id: I86584f17c5a90fe76923cfbf75c3b987da13fd95
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/82073
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Sascha Nowak <sascha.nowak@netlogix.de>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .phpstorm.meta.php                            |   2 +
 .../Classes/Modules/CacheModule.php           |   5 +-
 .../Classes/Modules/Debug/PageTitle.php       |  13 +-
 .../Modules/Info/GeneralInformation.php       |  22 ++--
 .../Classes/Modules/PreviewModule.php         |  15 +--
 .../Modules/TsDebug/TypoScriptWaterfall.php   |   5 +-
 .../Templates/Modules/Info/General.html       |   4 +
 .../Tests/Unit/Modules/PreviewModuleTest.php  |   5 +-
 ...stTSFEMembersMarkedInternalOrRead-only.rst |   1 +
 ...ture-102628-CacheInstructionMiddleware.rst |  42 +++++++
 .../Classes/Cache/CacheInstruction.php        |  71 +++++++++++
 .../ContentObject/ContentObjectRenderer.php   |   4 +-
 .../TypoScriptFrontendController.php          | 118 ++++++++----------
 .../AfterCacheableContentIsGeneratedEvent.php |   2 +-
 .../Event/AfterCachedPageIsPersistedEvent.php |  10 +-
 ...houldUseCachedPageDataIfAvailableEvent.php |   2 +-
 .../frontend/Classes/Http/RequestHandler.php  |   2 +-
 .../Middleware/BackendUserAuthenticator.php   |   7 +-
 .../Middleware/PageArgumentValidator.php      |  54 ++++----
 .../TypoScriptFrontendInitialization.php      |  32 +++--
 .../TypoScriptFrontendControllerTest.php      |   5 +-
 .../ContentObjectRendererTest.php             |   9 +-
 .../Middleware/PageArgumentValidatorTest.php  |  21 +---
 .../Classes/Service/RedirectService.php       |   3 +
 .../Classes/Middleware/WorkspacePreview.php   |  14 +--
 25 files changed, 283 insertions(+), 185 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.0/Feature-102628-CacheInstructionMiddleware.rst
 create mode 100644 typo3/sysext/frontend/Classes/Cache/CacheInstruction.php

diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php
index 7453ce921de5..320f276bd25f 100644
--- a/.phpstorm.meta.php
+++ b/.phpstorm.meta.php
@@ -88,6 +88,7 @@ namespace PHPSTORM_META {
         'moduleData',
         'frontend.controller',
         'frontend.typoscript',
+        'frontend.cache.instruction',
     );
     override(\Psr\Http\Message\ServerRequestInterface::getAttribute(), map([
         'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class,
@@ -99,6 +100,7 @@ namespace PHPSTORM_META {
         'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class,
         'frontend.controller' => \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class,
         'frontend.typoscript' => \TYPO3\CMS\Core\TypoScript\FrontendTypoScript::class,
+        'frontend.cache.instruction' => \TYPO3\CMS\Frontend\Cache\CacheInstruction::class,
     ]));
 
     expectedArguments(
diff --git a/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php b/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php
index 85c86ac43c22..5163f340b4af 100644
--- a/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php
+++ b/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 
 class CacheModule extends AbstractModule implements PageSettingsProviderInterface, RequestEnricherInterface, ResourceProviderInterface
 {
@@ -84,7 +85,9 @@ class CacheModule extends AbstractModule implements PageSettingsProviderInterfac
     public function enrich(ServerRequestInterface $request): ServerRequestInterface
     {
         if ($this->configurationService->getConfigurationOption('cache', 'noCache')) {
-            $request = $request->withAttribute('noCache', true);
+            $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+            $cacheInstruction->disableCache('EXT:adminpanel: "No caching" disables cache.');
+            $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
         }
         return $request;
     }
diff --git a/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php b/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php
index a6b53f05a3ec..30cfce1375b8 100644
--- a/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php
+++ b/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php
@@ -24,7 +24,6 @@ use TYPO3\CMS\Adminpanel\ModuleApi\DataProviderInterface;
 use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
-use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
  * Admin Panel Page Title module for showing the Page title providers
@@ -62,13 +61,12 @@ class PageTitle extends AbstractSubModule implements DataProviderInterface
         $data = [
             'cacheEnabled' => true,
         ];
-        if ($this->isNoCacheEnabled()) {
+        if (!$this->isCachingAllowed($request)) {
             $data = [
                 'orderedProviders' => [],
                 'usedProvider' => null,
                 'skippedProviders' => [],
             ];
-
             $logRecords = GeneralUtility::makeInstance(InMemoryLogWriter::class)->getLogEntries();
             foreach ($logRecords as $logEntry) {
                 if ($logEntry->getComponent() === self::LOG_COMPONENT) {
@@ -100,13 +98,8 @@ class PageTitle extends AbstractSubModule implements DataProviderInterface
         return $view->render();
     }
 
-    protected function isNoCacheEnabled(): bool
-    {
-        return (bool)$this->getTypoScriptFrontendController()->no_cache;
-    }
-
-    protected function getTypoScriptFrontendController(): TypoScriptFrontendController
+    protected function isCachingAllowed(ServerRequestInterface $request): bool
     {
-        return $GLOBALS['TSFE'];
+        return $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
     }
 }
diff --git a/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php b/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php
index 91e7dbd6ef2a..c3f0c0f51c34 100644
--- a/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php
+++ b/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php
@@ -53,15 +53,16 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf
                     'pageUid' => $tsfe->id,
                     'pageType' => $tsfe->getPageArguments()->getPageType(),
                     'groupList' => implode(',', $frontendUserAspect->getGroupIds()),
-                    'noCache' => $this->isNoCacheEnabled(),
+                    'noCache' => !$this->isCachingAllowed($request),
+                    'noCacheReasons' => $request->getAttribute('frontend.cache.instruction')->getDisabledCacheReasons(),
                     'countUserInt' => count($tsfe->config['INTincScript'] ?? []),
                     'totalParsetime' => $this->getTimeTracker()->getParseTime(),
                     'feUser' => [
                         'uid' => $frontendUserAspect->get('id') ?: 0,
                         'username' => $frontendUserAspect->get('username') ?: '',
                     ],
-                    'imagesOnPage' => $this->collectImagesOnPage(),
-                    'documentSize' => $this->collectDocumentSize(),
+                    'imagesOnPage' => $this->collectImagesOnPage($request),
+                    'documentSize' => $this->collectDocumentSize($request),
                 ],
             ]
         );
@@ -106,7 +107,7 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf
      * Collects images from TypoScriptFrontendController and calculates the total size.
      * Returns human-readable image sizes for fluid template output
      */
-    protected function collectImagesOnPage(): array
+    protected function collectImagesOnPage(ServerRequestInterface $request): array
     {
         $imagesOnPage = [
             'files' => [],
@@ -115,7 +116,7 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf
             'totalSizeHuman' => GeneralUtility::formatSize(0),
         ];
 
-        if ($this->isNoCacheEnabled() === false) {
+        if ($this->isCachingAllowed($request)) {
             return $imagesOnPage;
         }
 
@@ -138,21 +139,20 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf
     }
 
     /**
-     * Gets the document size from the current page in a human readable format
+     * Gets the document size from the current page in a human-readable format
      */
-    protected function collectDocumentSize(): string
+    protected function collectDocumentSize(ServerRequestInterface $request): string
     {
         $documentSize = 0;
-        if ($this->isNoCacheEnabled() === true) {
+        if (!$this->isCachingAllowed($request)) {
             $documentSize = mb_strlen($this->getTypoScriptFrontendController()->content, 'UTF-8');
         }
-
         return GeneralUtility::formatSize($documentSize);
     }
 
-    protected function isNoCacheEnabled(): bool
+    protected function isCachingAllowed(ServerRequestInterface $request): bool
     {
-        return (bool)$this->getTypoScriptFrontendController()->no_cache;
+        return $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
     }
 
     protected function getTypoScriptFrontendController(): TypoScriptFrontendController
diff --git a/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php b/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php
index d6d7688556cb..ed8ebbf3a9b5 100644
--- a/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php
+++ b/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php
@@ -37,6 +37,7 @@ use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 
 /**
  * Admin Panel Preview Module
@@ -45,8 +46,6 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface,
 {
     use LoggerAwareTrait;
 
-    protected CacheManager $cacheManager;
-
     /**
      * module configuration, set on initialize
      *
@@ -61,10 +60,9 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface,
      */
     protected array $config;
 
-    public function injectCacheManager(CacheManager $cacheManager): void
-    {
-        $this->cacheManager = $cacheManager;
-    }
+    public function __construct(
+        protected readonly CacheManager $cacheManager,
+    ) {}
 
     public function getIconIdentifier(): string
     {
@@ -98,8 +96,11 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface,
         ];
         if ($this->config['showFluidDebug']) {
             // forcibly unset fluid caching as it does not care about the tsfe based caching settings
+            // @todo: Useless?! CacheManager is initialized via bootstrap already, TYPO3_CONF_VARS is not read again?
             unset($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['fluid_template']['frontend']);
-            $request = $request->withAttribute('noCache', true);
+            $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+            $cacheInstruction->disableCache('EXT:adminpanel: "Show fluid debug output" disables cache.');
+            $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
         }
         $this->initializeFrontendPreview(
             $this->config['showHiddenPages'],
diff --git a/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php b/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php
index 50cc86c43b86..1add7bd0c289 100644
--- a/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php
+++ b/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Adminpanel\Service\ConfigurationService;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 
 /**
  * Class TypoScriptWaterfall
@@ -53,7 +54,9 @@ class TypoScriptWaterfall extends AbstractSubModule implements RequestEnricherIn
     public function enrich(ServerRequestInterface $request): ServerRequestInterface
     {
         if ($this->getConfigurationOption('forceTemplateParsing')) {
-            $request = $request->withAttribute('noCache', true);
+            $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+            $cacheInstruction->disableCache('EXT:adminpanel: "Force TS rendering" disables cache.');
+            $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
         }
         $this->getTimeTracker()->LR = $this->getConfigurationOption('LR');
         return $request;
diff --git a/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html b/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html
index e3f8b1cf767b..6ce5aa7b025e 100644
--- a/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html
+++ b/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html
@@ -74,6 +74,10 @@
     \'LLL:EXT:adminpanel/Resources/Private/Language/locallang_info.xlf:noCache\': isCachedInfo
     }'}" debug="false"/>
 
+<f:if condition="{info.noCache}">
+    <f:render partial="Data/TableKeyValue" arguments="{label: 'Disabled Cache reasons', languageKey: languageKey, data: info.noCacheReasons}" debug="false"/>
+</f:if>
+
 <f:render partial="Data/TableKeyValue" arguments="{label: 'UserIntObjects', languageKey: languageKey, data: '{
     \'LLL:EXT:adminpanel/Resources/Private/Language/locallang_info.xlf:countUserInt\': info.countUserInt
     }'}" debug="false"/>
diff --git a/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php b/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php
index 1b6e361eab4a..0e1e539ec5e5 100644
--- a/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php
+++ b/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Adminpanel\Tests\Unit\Modules;
 
 use TYPO3\CMS\Adminpanel\Modules\PreviewModule;
 use TYPO3\CMS\Adminpanel\Service\ConfigurationService;
+use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -63,7 +64,7 @@ final class PreviewModuleTest extends UnitTestCase
         ];
         $configurationService->method('getConfigurationOption')->withAnyParameters()->willReturnMap($valueMap);
 
-        $previewModule = new PreviewModule();
+        $previewModule = new PreviewModule($this->createMock(CacheManager::class));
         $previewModule->injectConfigurationService($configurationService);
         $previewModule->enrich(new ServerRequest());
 
@@ -102,7 +103,7 @@ final class PreviewModuleTest extends UnitTestCase
             });
         GeneralUtility::setSingletonInstance(Context::class, $context);
 
-        $previewModule = new PreviewModule();
+        $previewModule = new PreviewModule($this->createMock(CacheManager::class));
         $previewModule->injectConfigurationService($configurationService);
         $previewModule->enrich($request);
     }
diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst
index 4a903d722065..d100bf6f466d 100644
--- a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst
+++ b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst
@@ -59,6 +59,7 @@ The following public class properties have been marked "read only":
 The following public class properties have been marked :php:`@internal` - in general
 all properties not listed above:
 
+* :php:`TypoScriptFrontendController->no_cache` - Use Request attribute :php:`frontend.cache.instruction` instead
 * :php:`TypoScriptFrontendController->additionalHeaderData`
 * :php:`TypoScriptFrontendController->additionalFooterData`
 * :php:`TypoScriptFrontendController->register`
diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102628-CacheInstructionMiddleware.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102628-CacheInstructionMiddleware.rst
new file mode 100644
index 000000000000..45b33153b41d
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102628-CacheInstructionMiddleware.rst
@@ -0,0 +1,42 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-102628-1702031683:
+
+===============================================
+Feature: #102628 - Cache instruction middleware
+===============================================
+
+See :issue:`102628`
+
+Description
+===========
+
+TYPO3 v13 introduces the new Frontend related Request attribute :php`frontend.cache.instruction`
+implemented by class :php:`TYPO3\CMS\Frontend\Cache\CacheInstruction`. This replaces the
+previous :php:`TyposcriptFrontendController->no_cache` property and boolean php:`noCache` Request
+attribute.
+
+Impact
+======
+
+The attribute can be used by middlewares to disable cache mechanics of the Frontend rendering.
+
+In early middlewares before :php:`typo3/cms-frontend/tsfe`, the attribute may or may not exist
+already. A safe way to interact with it is like this:
+
+.. code-block:: php
+
+    $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+    $cacheInstruction->disableCache('EXT:my-extension: My-reason disables caches.');
+    $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
+
+Extension with middlewares or other code after :php:`typo3/cms-frontend/tsfe` can assume the attribute to
+be set already. Usage example:
+
+.. code-block:: php
+
+    $cacheInstruction = $request->getAttribute('frontend.cache.instruction');
+    $cacheInstruction->disableCache('EXT:my-extension: My-reason disables caches.');
+
+
+.. index:: Frontend, PHP-API, ext:frontend
diff --git a/typo3/sysext/frontend/Classes/Cache/CacheInstruction.php b/typo3/sysext/frontend/Classes/Cache/CacheInstruction.php
new file mode 100644
index 000000000000..87c2338233f8
--- /dev/null
+++ b/typo3/sysext/frontend/Classes/Cache/CacheInstruction.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Frontend\Cache;
+
+/**
+ * This class contains cache details and is created or updated in middlewares of the
+ * Frontend rendering chain and added as Request attribute "frontend.cache.instruction".
+ *
+ * Its main goal is to *disable* the Frontend cache mechanisms in various scenarios, for
+ * instance when the admin panel is used to simulate access times, or when security
+ * mechanisms like cHash evaluation do not match.
+ */
+final class CacheInstruction
+{
+    private bool $allowCaching = true;
+    private array $disabledCacheReasons = [];
+
+    /**
+     * Instruct the core Frontend rendering to disable Frontend caching. Extensions with
+     * custom middlewares may set this.
+     *
+     * Note multiple cache layers are involved during Frontend rendering: For instance multiple
+     * TypoScript layers, the page cache and potentially others. Those caches are read from and
+     * written to within various middlewares. Depending on the position of a call to this method
+     * within the middleware stack, it can happen that some or all caches have already been
+     * read of written.
+     *
+     * Extensions that use this method should keep an eye on their middleware positions in the
+     * stack to estimate the performance impact of this call. It's of course best to not use
+     * the 'disable cache' mechanic at all, but to handle caching properly in extensions.
+     */
+    public function disableCache(string $reason): void
+    {
+        if (empty($reason)) {
+            throw new \RuntimeException(
+                'A non-empty reason must be given to disable cache. At least mention the extension name that triggers it.',
+                1701528694
+            );
+        }
+        $this->allowCaching = false;
+        $this->disabledCacheReasons[] = $reason;
+    }
+
+    public function isCachingAllowed(): bool
+    {
+        return $this->allowCaching;
+    }
+
+    /**
+     * @internal Typically only consumed by extensions like EXT:adminpanel
+     */
+    public function getDisabledCacheReasons(): array
+    {
+        return $this->disabledCacheReasons;
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
index 038f0d57c24e..60186e970edf 100644
--- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
+++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
@@ -689,7 +689,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
         }
 
         // Store cache
-        if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) {
+        if ($cacheConfiguration !== null && $this->getRequest()->getAttribute('frontend.cache.instruction')->isCachingAllowed()) {
             $key = $this->calculateCacheKey($cacheConfiguration);
             if (!empty($key)) {
                 $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
@@ -5497,7 +5497,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
      */
     protected function getFromCache(array $configuration)
     {
-        if ($this->getTypoScriptFrontendController()->no_cache) {
+        if (!$this->getRequest()->getAttribute('frontend.cache.instruction')->isCachingAllowed()) {
             return false;
         }
         $cacheKey = $this->calculateCacheKey($configuration);
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index 7bcc7e742f63..65a6b7991cba 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -74,7 +74,6 @@ use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Utility\RootlineUtility;
-use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
 use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
 use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;
@@ -113,14 +112,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     protected SiteLanguage $language;
     protected PageArguments $pageArguments;
 
-    /**
-     * Page will not be cached. Write only TRUE. Never clear value (some other
-     * code might have reasons to set it TRUE).
-     * @var bool
-     * @internal
-     */
-    public $no_cache = false;
-
     /**
      * Rootline of page records all the way to the root.
      *
@@ -399,7 +390,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      */
     public function __construct(Context $context, Site $site, SiteLanguage $siteLanguage, PageArguments $pageArguments)
     {
-        $this->initializeContext($context);
+        $this->context = $context;
         $this->site = $site;
         $this->language = $siteLanguage;
         $this->setPageArguments($pageArguments);
@@ -408,14 +399,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $this->initCaches();
     }
 
-    private function initializeContext(Context $context): void
-    {
-        $this->context = $context;
-        if (!$this->context->hasAspect('frontend.preview')) {
-            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
-        }
-    }
-
     protected function initPageRenderer(): void
     {
         if ($this->pageRenderer !== null) {
@@ -870,7 +853,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     }
 
     /**
-     * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions. That data can be used inside a page-not-found handler
+     * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions.
+     * That data can be used inside a page-not-found handler
      *
      * @param string|null $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
      * @return array Summary of why page access was not allowed.
@@ -890,7 +874,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class);
             foreach ($combinedRecords as $k => $pagerec) {
                 // If $k=0 then it is the very first page the original ID was pointing at and that will get a full check of course
-                // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the extendToSubpages flag set, hence checked only then!
+                // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the
+                // extendToSubpages flag set, hence checked only then!
                 if (!$k || $pagerec['extendToSubpages']) {
                     if ($pagerec['hidden'] ?? false) {
                         $output['hidden'][$pagerec['uid']] = true;
@@ -1009,6 +994,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         }
 
         $site = $this->getSite();
+        $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
 
         $tokenizer = new LossyTokenizer();
         $treeBuilder = GeneralUtility::makeInstance(SysTemplateTreeBuilder::class);
@@ -1017,9 +1003,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
         /** @var PhpFrontend|null $typoscriptCache */
         $typoscriptCache = null;
-        if (!$this->no_cache) {
-            // $this->no_cache = true might have been set by earlier TypoScriptFrontendInitialization middleware.
-            // This means we don't do fancy cache stuff, calculate full TypoScript and ignore page cache.
+        if ($isCachingAllowed) {
+            // disableCache() might have been called by earlier middlewares. This means we don't do fancy cache
+            // stuff, calculate full TypoScript and don't get() from nor set() to typoscript and page cache.
             /** @var PhpFrontend|null $typoscriptCache */
             $typoscriptCache = $cacheManager->getCache('typoscript');
         }
@@ -1048,7 +1034,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $flatConstants = [];
         $serializedConstantConditionList = '';
         $gotConstantFromCache = false;
-        if (!$this->no_cache && $constantConditionIncludeTree = $typoscriptCache->require($constantConditionIncludeListCacheIdentifier)) {
+        if ($isCachingAllowed && $constantConditionIncludeTree = $typoscriptCache->require($constantConditionIncludeListCacheIdentifier)) {
             // We got the flat list of all constants conditions for this TypoScript combination from cache. Good. We traverse
             // this list to calculate "current" condition verdicts. With a hash of this list together with a hash of the
             // TypoScript sys_templates, we try to retrieve the full constants TypoScript from cache.
@@ -1068,12 +1054,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 $gotConstantFromCache = true;
             }
         }
-        if ($this->no_cache || !$gotConstantFromCache) {
+        if (!$isCachingAllowed || !$gotConstantFromCache) {
             // We did not get constants from cache, or are not allowed to use cache. We have to build constants from scratch.
             // This means we'll fetch the full constants include tree (from cache if possible), register the condition
             // matcher and register the AST builder and traverse include tree to retrieve constants AST and calculate
             // 'flat constants' from it. Both are cached if allowed afterwards for the 'if' above to kick in next time.
-            // $typoscriptCache can be null here with no_cache=1.
             $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $tokenizer, $site, $typoscriptCache);
             $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
             $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
@@ -1085,12 +1070,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             // children for not matching conditions, which is important to create the correct AST.
             $includeTreeTraverserConditionVerdictAware->traverse($constantIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors);
             $constantsAst = $constantAstBuilderVisitor->getAst();
-            // @internal Dispatch and experimental event allowing listeners to still change the constants AST,
+            // @internal Dispatch an experimental event allowing listeners to still change the constants AST,
             //           to for instance implement nested constants if really needed. Note this event may change
             //           or vanish later without further notice.
             $constantsAst = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(new ModifyTypoScriptConstantsEvent($constantsAst))->getConstantsAst();
             $flatConstants = $constantsAst->flatten();
-            if (!$this->no_cache) {
+            if ($isCachingAllowed) {
                 // We are allowed to cache and can create both the full list of conditions, plus the constant AST and flat constant
                 // list cache entry. To do that, we need all (!) conditions, but the above ConditionVerdictAwareIncludeTreeTraverser
                 // did not find nested conditions if an upper condition did not match. We thus have to traverse include tree a
@@ -1121,7 +1106,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $setupConditionIncludeListCacheIdentifier = 'setup-condition-include-list-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList);
         $setupConditionList = [];
         $gotSetupConditionsFromCache = false;
-        if (!$this->no_cache && $setupConditionIncludeTree = $typoscriptCache->require($setupConditionIncludeListCacheIdentifier)) {
+        if ($isCachingAllowed && $setupConditionIncludeTree = $typoscriptCache->require($setupConditionIncludeListCacheIdentifier)) {
             // We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse
             // this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier.
             $includeTreeTraverserVisitors = [];
@@ -1138,12 +1123,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $gotSetupConditionsFromCache = true;
         }
         $setupIncludeTree = null;
-        if ($this->no_cache || !$gotSetupConditionsFromCache) {
+        if (!$isCachingAllowed || !$gotSetupConditionsFromCache) {
             // We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup
             // condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible),
             // register the constant substitution visitor, and register condition matcher and register the condition
             // accumulator visitor.
-            // $typoscriptCache can be null here with no_cache=1.
             $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache);
             $includeTreeTraverserVisitors = [];
             $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
@@ -1167,7 +1151,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         // obviously the page id.
         $this->lock = GeneralUtility::makeInstance(ResourceMutex::class);
         $this->newHash = $this->createHashBase($sysTemplateRows, $constantConditionList, $setupConditionList);
-        if (!$this->no_cache) {
+        if ($isCachingAllowed) {
             if ($this->shouldAcquireCacheData($request)) {
                 // Try to get a page cache row.
                 $this->getTimeTracker()->push('Cache Row');
@@ -1213,16 +1197,15 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $this->lock->acquireLock('pages', $this->newHash);
         }
 
-        if ($this->no_cache || empty($this->config) || $this->isINTincScript()) {
-            // We don't need the full setup AST in many cached scenarios. However, if no_cache is set, if no page cache
-            // entry could be loaded, if the page cache entry has _INT object, or if the user forced template
-            // parsing (adminpanel), then we still need the full setup AST. If there is "just" an _INT object, we can
-            // use a possible cache entry for the setup AST, which speeds up _INT parsing quite a bit. In other cases
-            // we calculate full setup AST and cache it if allowed.
+        if (!$isCachingAllowed || empty($this->config) || $this->isINTincScript()) {
+            // We don't need the full setup AST in many cached scenarios. However, if caching is not allowed, if no page
+            // cache entry could be loaded or if the page cache entry has _INT object, then we still need the full setup AST.
+            // If there is "just" an _INT object, we can use a possible cache entry for the setup AST, which speeds up _INT
+            // parsing quite a bit. In other cases we calculate full setup AST and cache it if allowed.
             $setupTypoScriptCacheIdentifier = 'setup-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList . serialize($setupConditionList));
             $gotSetupFromCache = false;
             $setupArray = [];
-            if (!$this->no_cache) {
+            if ($isCachingAllowed) {
                 // We need AST, but we are allowed to potentially get it from cache.
                 if ($setupTypoScriptCache = $typoscriptCache->require($setupTypoScriptCacheIdentifier)) {
                     $frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']);
@@ -1230,11 +1213,10 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                     $gotSetupFromCache = true;
                 }
             }
-            if ($this->no_cache || !$gotSetupFromCache) {
+            if (!$isCachingAllowed || !$gotSetupFromCache) {
                 // We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup
                 // IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not.
-                if ($this->no_cache || $setupIncludeTree === null) {
-                    // $typoscriptCache can be null here with no_cache=1.
+                if (!$isCachingAllowed || $setupIncludeTree === null) {
                     $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache);
                 }
                 $includeTreeTraverserConditionVerdictAwareVisitors = [];
@@ -1279,7 +1261,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                     $setupAst->addChild($typesNode);
                 }
                 $setupArray = $setupAst->toArray();
-                if (!$this->no_cache) {
+                if ($isCachingAllowed) {
                     // Write cache entry for AST and its array representation, we're allowed to do it.
                     $typoscriptCache->set($setupTypoScriptCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupArray]), '\'\\') . '\');');
                 }
@@ -1322,9 +1304,10 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $frontendTypoScript->setSetupArray($setupArray);
         }
 
-        // Set $this->no_cache TRUE if the config.no_cache value is set!
-        if (!$this->no_cache && ($this->config['config']['no_cache'] ?? false)) {
-            $this->set_no_cache('config.no_cache is set', true);
+        // Disable cache if config.no_cache is set!
+        if ($this->config['config']['no_cache'] ?? false) {
+            $cacheInstruction = $request->getAttribute('frontend.cache.instruction');
+            $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"');
         }
 
         // Auto-configure settings when a site is configured
@@ -1389,8 +1372,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      */
     protected function shouldAcquireCacheData(ServerRequestInterface $request): bool
     {
-        // Trigger event for possible by-pass of requiring of page cache (for re-caching purposes)
-        $event = new ShouldUseCachedPageDataIfAvailableEvent($request, $this, !$this->no_cache);
+        // Trigger event for possible by-pass of requiring of page cache.
+        $event = new ShouldUseCachedPageDataIfAvailableEvent($request, $this, $request->getAttribute('frontend.cache.instruction')->isCachingAllowed());
         GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event);
         return $event->shouldUseCachedPageData();
     }
@@ -1812,7 +1795,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     {
         $this->setAbsRefPrefix();
         $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
-        $event = new AfterCacheableContentIsGeneratedEvent($request, $this, $this->newHash, !$this->no_cache);
+        $usePageCache = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
+        $event = new AfterCacheableContentIsGeneratedEvent($request, $this, $this->newHash, $usePageCache);
         $event = $eventDispatcher->dispatch($event);
 
         // Processing if caching is enabled
@@ -2070,7 +2054,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      *
      * @internal
      */
-    public function applyHttpHeadersToResponse(ResponseInterface $response): ResponseInterface
+    public function applyHttpHeadersToResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
     {
         $response = $response->withHeader('Content-Type', $this->contentType);
         // Set header for content language unless disabled
@@ -2085,7 +2069,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         }
 
         // Set cache related headers to client (used to enable proxy / client caching!)
-        $headers = $this->getCacheHeaders();
+        $headers = $this->getCacheHeaders($request);
         foreach ($headers as $header => $value) {
             $response = $response->withHeader($header, $value);
         }
@@ -2108,11 +2092,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     /**
      * Get cache headers good for client/reverse proxy caching.
      */
-    protected function getCacheHeaders(): array
+    protected function getCacheHeaders(ServerRequestInterface $request): array
     {
         $headers = [];
         // Getting status whether we can send cache control headers for proxy caching:
-        $doCache = $this->isStaticCacheble();
+        $doCache = $this->isStaticCacheble($request);
         $isBackendUserLoggedIn = $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
         $isInWorkspace = $this->context->getPropertyFromAspect('workspace', 'isOffline', false);
         // Finally, when backend users are logged in, do not send cache headers at all (Admin Panel might be displayed for instance).
@@ -2139,8 +2123,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                     $this->getTimeTracker()->setTSlogMessage('Cache-headers with max-age "' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']) . '" would have been sent');
                 } else {
                     $reasonMsg = [];
-                    if ($this->no_cache) {
-                        $reasonMsg[] = 'Caching disabled (no_cache).';
+                    if (!$request->getAttribute('frontend.cache.instruction')->isCachingAllowed()) {
+                        $reasonMsg[] = 'Caching disabled.';
                     }
                     if ($this->isINTincScript()) {
                         $reasonMsg[] = '*_INT object(s) on page.';
@@ -2169,9 +2153,10 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      *
      * @internal
      */
-    public function isStaticCacheble(): bool
+    public function isStaticCacheble(ServerRequestInterface $request): bool
     {
-        return !$this->no_cache && !$this->isINTincScript() && !$this->context->getAspect('frontend.user')->isUserOrGroupSet();
+        $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
+        return $isCachingAllowed && !$this->isINTincScript() && !$this->context->getAspect('frontend.user')->isUserOrGroupSet();
     }
 
     /**
@@ -2258,9 +2243,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * Sets the cache-flag to 1. Could be called from user-included php-files in order to ensure that a page is not cached.
      *
      * @param string $reason An optional reason to be written to the log.
-     * @param bool $internalRequest Whether the request is internal or not (true should only be used by core calls).
+     * @todo: deprecate
      */
-    public function set_no_cache(string $reason = '', bool $internalRequest = false): void
+    public function set_no_cache(string $reason = ''): void
     {
         $warning = '';
         $context = [];
@@ -2284,24 +2269,19 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             }
             $context['line'] = $trace[0]['line'];
         }
-        if (!$internalRequest && $GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']) {
+        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']) {
             $warning .= ' However, $TYPO3_CONF_VARS[\'FE\'][\'disableNoCacheParameter\'] is set, so it will be ignored!';
             $this->getTimeTracker()->setTSlogMessage($warning, LogLevel::NOTICE);
         } else {
             $warning .= ' Caching is disabled!';
-            $this->disableCache();
+            /** @var ServerRequestInterface $request */
+            $request = $GLOBALS['TYPO3_REQUEST'];
+            $cacheInstruction = $request->getAttribute('frontend.cache.instruction');
+            $cacheInstruction->disableCache('EXT:frontend: Caching disabled using deprecated set_no_cache().');
         }
         $this->logger->notice($warning, $context);
     }
 
-    /**
-     * Disables caching of the current page.
-     */
-    protected function disableCache(): void
-    {
-        $this->no_cache = true;
-    }
-
     /**
      * Sets the cache-timeout in seconds
      *
diff --git a/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php
index 77a34659fc63..22c89be2f0eb 100644
--- a/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php
+++ b/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php
@@ -21,7 +21,7 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
- * Event that allows to enhance or change content (also depending if caching is enabled).
+ * Event that allows to enhance or change content (also depending on enabled caching).
  * Think of $this->isCachingEnabled() as the same as $TSFE->no_cache.
  * Depending on disable or enabling caching, the cache is then not stored in the pageCache.
  */
diff --git a/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php
index 18543c1a6a03..fd77aded595d 100644
--- a/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php
+++ b/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php
@@ -21,12 +21,12 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
- * Event that is used directly after all cached content is stored in
- * the page cache.
+ * Event that is used directly after all cached content is stored in the page cache.
  *
- * If a page is called from the cache, this event is NOT fired.
- * This event is also NOT FIRED when $TSFE->no_cache (or manipulated via AfterCacheableContentIsGeneratedEvent)
- * is set.
+ * NOT fired, if:
+ * * A page is called from the cache
+ * * Caching is disabled using 'frontend.cache.instruction' request attribute, which can
+ *   be set by various middlewares or AfterCacheableContentIsGeneratedEvent
  */
 final class AfterCachedPageIsPersistedEvent
 {
diff --git a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php
index ba58f88dab7a..b5602b3d7f2d 100644
--- a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php
+++ b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php
@@ -22,7 +22,7 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
  * Event to allow listeners to disable the loading of cached page data when a page is requested.
- * Does not have any effect if "no_cache" is activated, or if there is no cached version of a page.
+ * Does not have any effect if caching is disabled, or if there is no cached version of a page.
  */
 final class ShouldUseCachedPageDataIfAvailableEvent
 {
diff --git a/typo3/sysext/frontend/Classes/Http/RequestHandler.php b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
index 4624459a6264..84618a74ba4e 100644
--- a/typo3/sysext/frontend/Classes/Http/RequestHandler.php
+++ b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
@@ -179,7 +179,7 @@ class RequestHandler implements RequestHandlerInterface
 
         // Create a default Response object and add headers and body to it
         $response = new Response();
-        $response = $controller->applyHttpHeadersToResponse($response);
+        $response = $controller->applyHttpHeadersToResponse($request, $response);
         $this->displayPreviewInfoMessage($controller);
         $response->getBody()->write($controller->content);
         return $response;
diff --git a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php
index dd2ee67f000a..039b06befcff 100644
--- a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php
+++ b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php
@@ -28,6 +28,7 @@ use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Http\NormalizedParams;
 use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 
 /**
  * This middleware authenticates a Backend User (be_user) (pre)-viewing a frontend page.
@@ -70,9 +71,11 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut
                 && (strtolower($request->getServerParams()['HTTP_CACHE_CONTROL'] ?? '') === 'no-cache'
                     || strtolower($request->getServerParams()['HTTP_PRAGMA'] ?? '') === 'no-cache')
             ) {
-                // Detecting if shift-reload has been clicked to set noCache attribute if so.
+                // Detecting if shift-reload has been clicked to disable caching if so.
                 // This is only done if a backend user is logged in to prevent DoS-attacks for "casual" requests.
-                $request = $request->withAttribute('noCache', true);
+                $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+                $cacheInstruction->disableCache('EXT:frontend: Logged in backend user forced reload disabled cache.');
+                $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
             }
         }
 
diff --git a/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php b/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php
index 3d2b91b3b634..ee2b25c46539 100644
--- a/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php
+++ b/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php
@@ -23,12 +23,11 @@ use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
-use Psr\Log\LogLevel;
 use TYPO3\CMS\Core\Http\RedirectResponse;
 use TYPO3\CMS\Core\Routing\PageArguments;
-use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
@@ -40,14 +39,8 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
 {
     use LoggerAwareTrait;
 
-    /**
-     * @var bool will be used to set $TSFE->no_cache later-on
-     */
-    protected bool $disableCache = false;
-
     public function __construct(
-        protected readonly CacheHashCalculator $cacheHashCalculator,
-        protected readonly TimeTracker $timeTracker
+        private readonly CacheHashCalculator $cacheHashCalculator,
     ) {}
 
     /**
@@ -55,10 +48,10 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
      */
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
     {
-        $this->disableCache = (bool)$request->getAttribute('noCache', false);
+        $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+        $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
         $pageNotFoundOnValidationError = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] ?? true);
-        /** @var PageArguments $pageArguments */
-        $pageArguments = $request->getAttribute('routing', null);
+        $pageArguments = $request->getAttribute('routing');
         if (!($pageArguments instanceof PageArguments)) {
             // Page Arguments must be set in order to validate. This middleware only works if PageArguments
             // is available, and is usually combined with the Page Resolver middleware
@@ -68,12 +61,12 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
                 ['code' => PageAccessFailureReasons::INVALID_PAGE_ARGUMENTS]
             );
         }
-        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter'] ?? true) {
-            $cachingDisabledByRequest = false;
-        } else {
-            $cachingDisabledByRequest = $pageArguments->getArguments()['no_cache'] ?? $request->getParsedBody()['no_cache'] ?? false;
+        if (!($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter'] ?? true)
+            && ($pageArguments->getArguments()['no_cache'] ?? $request->getParsedBody()['no_cache'] ?? false)
+        ) {
+            $cacheInstruction->disableCache('EXT:frontend: Caching disabled by no_cache query argument.');
         }
-        if (($cachingDisabledByRequest || $this->disableCache) && !$pageNotFoundOnValidationError) {
+        if (!$cacheInstruction->isCachingAllowed() && !$pageNotFoundOnValidationError) {
             // No need to test anything if caching was already disabled.
             return $handler->handle($request);
         }
@@ -84,15 +77,14 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
             $relevantParametersForCacheHashArgument = $this->getRelevantParametersForCacheHashCalculation($pageArguments);
             if ($cHash !== '') {
                 if (empty($relevantParametersForCacheHashArgument)) {
-                    // cHash was given, but nothing to be calculated, so let's do a redirect to the current page
-                    // but without the cHash
+                    // cHash was given, but nothing to be calculated, so let's do a redirect to the current page but without the cHash
                     $this->logger->notice('The incoming cHash "{hash}" is given but not needed. cHash is unset', ['hash' => $cHash]);
                     $uri = $request->getUri();
                     unset($queryParams['cHash']);
                     $uri = $uri->withQuery(HttpUtility::buildQueryString($queryParams));
                     return new RedirectResponse($uri, 308);
                 }
-                if (!$this->evaluateCacheHashParameter($cHash, $relevantParametersForCacheHashArgument, $pageNotFoundOnValidationError)) {
+                if (!$this->evaluateCacheHashParameter($cacheInstruction, $cHash, $relevantParametersForCacheHashArgument, $pageNotFoundOnValidationError)) {
                     return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
                         $request,
                         'Request parameters could not be validated (&cHash comparison failed)',
@@ -100,7 +92,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
                     );
                 }
             // No cHash given but was required
-            } elseif (!$this->evaluatePageArgumentsWithoutCacheHash($pageArguments, $pageNotFoundOnValidationError)) {
+            } elseif (!$this->evaluatePageArgumentsWithoutCacheHash($cacheInstruction, $pageArguments, $pageNotFoundOnValidationError)) {
                 return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
                     $request,
                     'Request parameters could not be validated (&cHash empty)',
@@ -109,7 +101,6 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
             }
         }
 
-        $request = $request->withAttribute('noCache', $this->disableCache);
         return $handler->handle($request);
     }
 
@@ -134,7 +125,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
      * @param bool $pageNotFoundOnCacheHashError see $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']
      * @return bool if false, then a PageNotFound response is triggered
      */
-    protected function evaluateCacheHashParameter(string $cHash, array $relevantParameters, bool $pageNotFoundOnCacheHashError): bool
+    protected function evaluateCacheHashParameter(CacheInstruction $cacheInstruction, string $cHash, array $relevantParameters, bool $pageNotFoundOnCacheHashError): bool
     {
         $calculatedCacheHash = $this->cacheHashCalculator->calculateCacheHash($relevantParameters);
         if (hash_equals($calculatedCacheHash, $cHash)) {
@@ -145,8 +136,8 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
             return false;
         }
         // Caching is disabled now (but no 404)
-        $this->disableCache = true;
-        $this->timeTracker->setTSlogMessage('The incoming cHash "' . $cHash . '" and calculated cHash "' . $calculatedCacheHash . '" did not match, so caching was disabled. The fieldlist used was "' . implode(',', array_keys($relevantParameters)) . '"', LogLevel::ERROR);
+        $cacheInstruction->disableCache('EXT:frontend: Incoming cHash "' . $cHash . '" and calculated cHash "' . $calculatedCacheHash . '" did not match.' .
+            ' The field list used was "' . implode(',', array_keys($relevantParameters)) . '". Caching is disabled.');
         return true;
     }
 
@@ -156,9 +147,8 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
      * Should only be called if NO cHash parameter is given.
      *
      * @param array<string, string|array> $dynamicArguments
-     * @param bool $pageNotFoundOnCacheHashError
      */
-    protected function evaluateQueryParametersWithoutCacheHash(array $dynamicArguments, bool $pageNotFoundOnCacheHashError): bool
+    protected function evaluateQueryParametersWithoutCacheHash(CacheInstruction $cacheInstruction, array $dynamicArguments, bool $pageNotFoundOnCacheHashError): bool
     {
         if (!$this->cacheHashCalculator->doParametersRequireCacheHash(HttpUtility::buildQueryString($dynamicArguments))) {
             return true;
@@ -168,8 +158,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
             return false;
         }
         // Caching is disabled now (but no 404)
-        $this->disableCache = true;
-        $this->timeTracker->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', LogLevel::ERROR);
+        $cacheInstruction->disableCache('EXT:frontend: No cHash query argument was sent for GET vars though required. Caching is disabled.');
         return true;
     }
 
@@ -179,11 +168,11 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
      *
      * Is only called if NO cHash parameter is given.
      */
-    protected function evaluatePageArgumentsWithoutCacheHash(PageArguments $pageArguments, bool $pageNotFoundOnCacheHashError): bool
+    protected function evaluatePageArgumentsWithoutCacheHash(CacheInstruction $cacheInstruction, PageArguments $pageArguments, bool $pageNotFoundOnCacheHashError): bool
     {
         // legacy behaviour
         if (!($GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['enforceValidation'] ?? false)) {
-            return $this->evaluateQueryParametersWithoutCacheHash($pageArguments->getDynamicArguments(), $pageNotFoundOnCacheHashError);
+            return $this->evaluateQueryParametersWithoutCacheHash($cacheInstruction, $pageArguments->getDynamicArguments(), $pageNotFoundOnCacheHashError);
         }
         $relevantParameters = $this->getRelevantParametersForCacheHashCalculation($pageArguments);
         // There are parameters that would be needed for the current page, but no cHash is given.
@@ -198,8 +187,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
             return true;
         }
         // Caching is disabled now (but no 404)
-        $this->disableCache = true;
-        $this->timeTracker->setTSlogMessage('No &cHash parameter was sent for given query parameters, so caching is disabled', LogLevel::ERROR);
+        $cacheInstruction->disableCache('EXT:frontend: No cHash query argument was sent for given query parameters. Caching is disabled');
         return true;
     }
 }
diff --git a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
index 9a7b98b01cf3..98ed0694204b 100644
--- a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
+++ b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
@@ -26,6 +26,8 @@ use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
@@ -50,10 +52,25 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface
      */
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
     {
+        // The cache information attribute may be set by previous middlewares already. Make sure we have one from now on.
+        $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+        $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
+
+        // Make sure frontend.preview is given from now on.
+        if (!$this->context->hasAspect('frontend.preview')) {
+            $this->context->setAspect('frontend.preview', new PreviewAspect());
+        }
+        // If the frontend is showing a preview, caching MUST be disabled.
+        if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) {
+            // @todo: To disentangle this, the preview aspect could be dropped and middlewares that set isPreview true
+            //        could directly set $cacheInstruction->disableCache() instead.
+            $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to enabled frontend.preview aspect isPreview.');
+        }
+
         $GLOBALS['TYPO3_REQUEST'] = $request;
         /** @var Site $site */
-        $site = $request->getAttribute('site', null);
-        $pageArguments = $request->getAttribute('routing', null);
+        $site = $request->getAttribute('site');
+        $pageArguments = $request->getAttribute('routing');
         if (!$pageArguments instanceof PageArguments) {
             // Page Arguments must be set in order to validate. This middleware only works if PageArguments
             // is available, and is usually combined with the Page Resolver middleware
@@ -71,17 +88,6 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface
             $request->getAttribute('language', $site->getDefaultLanguage()),
             $pageArguments
         );
-        if ($pageArguments->getArguments()['no_cache'] ?? $request->getParsedBody()['no_cache'] ?? false) {
-            $controller->set_no_cache('&no_cache=1 has been supplied, so caching is disabled! URL: "' . (string)$request->getUri() . '"');
-        }
-        // Usually only set by the PageArgumentValidator
-        if ($request->getAttribute('noCache', false)) {
-            $controller->no_cache = true;
-        }
-        // If the frontend is showing a preview, caching MUST be disabled.
-        if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) {
-            $controller->set_no_cache('Preview active', true);
-        }
         $directResponse = $controller->determineId($request);
         if ($directResponse) {
             return $directResponse;
diff --git a/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php
index b6a77fa28708..f896a400dce9 100644
--- a/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php
+++ b/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Frontend\Tests\Functional\Controller;
 
 use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
 use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
@@ -390,7 +391,9 @@ alert(yes);', $body);
 
         $request = (new InternalRequest('https://website.local/en/'))->withPageId($pid);
         if ($nocache) {
-            $request = $request->withAttribute('noCache', true);
+            $cacheInstruction = new CacheInstruction();
+            $cacheInstruction->disableCache('EXT:frontend: Testing disables caching.');
+            $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
         }
         $this->executeFrontendSubRequest($request);
         self::assertSame($expectedRootLine, $GLOBALS['TSFE']->rootLine);
diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
index 9404fe53bfc0..79107e053ac6 100644
--- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
@@ -62,6 +62,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
 use TYPO3\CMS\Frontend\ContentObject\CaseContentObject;
 use TYPO3\CMS\Frontend\ContentObject\ContentContentObject;
@@ -2709,6 +2710,7 @@ final class ContentObjectRendererTest extends UnitTestCase
             ContentObjectRenderer::class,
             [
                 'calculateCacheKey',
+                'getRequest',
                 'getTypoScriptFrontendController',
             ]
         );
@@ -2717,13 +2719,18 @@ final class ContentObjectRendererTest extends UnitTestCase
             ->method('calculateCacheKey')
             ->with($conf)
             ->willReturn($cacheKey);
+        $request = (new ServerRequest())->withAttribute('frontend.cache.instruction', new CacheInstruction());
+        $subject
+            ->expects(self::once())
+            ->method('getRequest')
+            ->willReturn($request);
         $typoScriptFrontendController = $this->createMock(TypoScriptFrontendController::class);
         $typoScriptFrontendController
             ->expects(self::exactly($times))
             ->method('addCacheTags')
             ->with($tags);
         $subject
-            ->expects(self::exactly($times + 1))
+            ->expects(self::exactly($times))
             ->method('getTypoScriptFrontendController')
             ->willReturn($typoScriptFrontendController);
         $cacheFrontend = $this->createMock(CacheFrontendInterface::class);
diff --git a/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php b/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php
index 62a473275b07..7810e27eb5da 100644
--- a/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php
@@ -25,29 +25,20 @@ use TYPO3\CMS\Core\Http\Response;
 use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Information\Typo3Information;
 use TYPO3\CMS\Core\Routing\PageArguments;
-use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Frontend\Middleware\PageArgumentValidator;
-use TYPO3\CMS\Frontend\Middleware\PageResolver;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
-use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 final class PageArgumentValidatorTest extends UnitTestCase
 {
     protected bool $resetSingletonInstances = true;
 
-    protected CacheHashCalculator $cacheHashCalculator;
-    protected TimeTracker $timeTrackerStub;
-    protected RequestHandlerInterface $responseOutputHandler;
-    protected PageResolver&AccessibleObjectInterface $subject;
+    private RequestHandlerInterface $responseOutputHandler;
 
     protected function setUp(): void
     {
         parent::setUp();
-        $this->timeTrackerStub = new TimeTracker(false);
-        $this->cacheHashCalculator = new CacheHashCalculator();
-
         // A request handler which only runs through
         $this->responseOutputHandler = new class () implements RequestHandlerInterface {
             public function handle(ServerRequestInterface $request): ResponseInterface
@@ -70,7 +61,7 @@ final class PageArgumentValidatorTest extends UnitTestCase
         $request = new ServerRequest($incomingUrl, 'GET');
         $request = $request->withAttribute('routing', $pageArguments);
 
-        $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub);
+        $subject = new PageArgumentValidator(new CacheHashCalculator());
         $subject->setLogger(new NullLogger());
 
         $response = $subject->process($request, $this->responseOutputHandler);
@@ -90,7 +81,7 @@ final class PageArgumentValidatorTest extends UnitTestCase
         $request = new ServerRequest($incomingUrl, 'GET');
         $request = $request->withAttribute('routing', $pageArguments);
 
-        $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub);
+        $subject = new PageArgumentValidator(new CacheHashCalculator());
         $typo3InformationMock = $this->getMockBuilder(Typo3Information::class)->disableOriginalConstructor()->getMock();
         $typo3InformationMock->expects(self::once())->method('getCopyrightYear')->willReturn('1999-20XX');
         GeneralUtility::addInstance(Typo3Information::class, $typo3InformationMock);
@@ -107,7 +98,7 @@ final class PageArgumentValidatorTest extends UnitTestCase
         $incomingUrl = 'https://king.com/lotus-flower/en/mr-magpie/bloom/';
         $request = new ServerRequest($incomingUrl, 'GET');
 
-        $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub);
+        $subject = new PageArgumentValidator(new CacheHashCalculator());
         $typo3InformationMock = $this->getMockBuilder(Typo3Information::class)->disableOriginalConstructor()->getMock();
         $typo3InformationMock->expects(self::once())->method('getCopyrightYear')->willReturn('1999-20XX');
         GeneralUtility::addInstance(Typo3Information::class, $typo3InformationMock);
@@ -127,7 +118,7 @@ final class PageArgumentValidatorTest extends UnitTestCase
         $request = new ServerRequest($incomingUrl, 'GET');
         $request = $request->withAttribute('routing', $pageArguments);
 
-        $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub);
+        $subject = new PageArgumentValidator(new CacheHashCalculator());
         $response = $subject->process($request, $this->responseOutputHandler);
         self::assertEquals(200, $response->getStatusCode());
     }
@@ -144,7 +135,7 @@ final class PageArgumentValidatorTest extends UnitTestCase
         $request = new ServerRequest($incomingUrl, 'GET');
         $request = $request->withAttribute('routing', $pageArguments);
 
-        $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub);
+        $subject = new PageArgumentValidator(new CacheHashCalculator());
         $typo3InformationMock = $this->getMockBuilder(Typo3Information::class)->disableOriginalConstructor()->getMock();
         $typo3InformationMock->expects(self::once())->method('getCopyrightYear')->willReturn('1999-20XX');
         GeneralUtility::addInstance(Typo3Information::class, $typo3InformationMock);
diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php
index d0af47f1dc14..1bedbb7197c5 100644
--- a/typo3/sysext/redirects/Classes/Service/RedirectService.php
+++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php
@@ -37,6 +37,7 @@ use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
 use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
@@ -386,6 +387,8 @@ class RedirectService implements LoggerAwareInterface
      */
     protected function bootFrontendController(SiteInterface $site, array $queryParams, ServerRequestInterface $originalRequest): TypoScriptFrontendController
     {
+        $cacheInstruction = $originalRequest->getAttribute('frontend.cache.instruction', new CacheInstruction());
+        $originalRequest = $originalRequest->withAttribute('frontend.cache.instruction', $cacheInstruction);
         $controller = GeneralUtility::makeInstance(
             TypoScriptFrontendController::class,
             GeneralUtility::makeInstance(Context::class),
diff --git a/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php b/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php
index 47b100170501..c1f922ceb69d 100644
--- a/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php
+++ b/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php
@@ -38,6 +38,7 @@ use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
 use TYPO3\CMS\Core\Routing\RouteResultInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Workspaces\Authentication\PreviewUserAuthentication;
 
@@ -74,7 +75,6 @@ class WorkspacePreview implements MiddlewareInterface
      */
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
     {
-        $addInformationAboutDisabledCache = false;
         $keyword = $this->getPreviewInputCode($request);
         $setCookieOnCurrentRequest = false;
         $normalizedParams = $request->getAttribute('normalizedParams');
@@ -118,20 +118,16 @@ class WorkspacePreview implements MiddlewareInterface
             $GLOBALS['BE_USER']->setTemporaryWorkspace(0);
             // Register the backend user as aspect
             $this->setBackendUserAspect($context, $GLOBALS['BE_USER']);
-            // Caching is disabled, because otherwise generated URLs could include the keyword parameter
-            $request = $request->withAttribute('noCache', true);
-            $addInformationAboutDisabledCache = true;
+            $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
+            $cacheInstruction->disableCache('ext:workspaces: Disabled FE cache with BE_USER previewing live workspace');
+            $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
             $setCookieOnCurrentRequest = false;
         }
 
         $response = $handler->handle($request);
 
-        $tsfe = $this->getTypoScriptFrontendController();
-        if ($tsfe !== null && $addInformationAboutDisabledCache) {
-            $tsfe->set_no_cache('GET Parameter ADMCMD_prev=LIVE was given', true);
-        }
-
         // Add an info box to the frontend content
+        $tsfe = $this->getTypoScriptFrontendController();
         if ($tsfe !== null && $context->getPropertyFromAspect('workspace', 'isOffline', false)) {
             $previewInfo = $this->renderPreviewInfo($tsfe, $request->getUri());
             $body = $response->getBody();
-- 
GitLab