diff --git a/phpstan.neon b/phpstan.neon
index ba1fee1893fbaa741ef8064a6222de216d614895..ffef0a8409a1037937cef065178ac3c79cd0f557 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -254,3 +254,8 @@ parameters:
       message: "#^Parameter \\#1 \\$result of method TYPO3\\\\CMS\\\\Backend\\\\Controller\\\\File\\\\FileController\\:\\:flattenResultDataValue\\(\\) expects bool\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder, TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile given\\.$#"
       count: 1
       path: typo3/sysext/backend/Classes/Controller/File/FileController.php
+    -
+      # Obsolete in v12, when either entire Request or __construct() are declared final
+      message: "#^Unsafe usage of new static\\(\\)\\.$#"
+      count: 23
+      path: typo3/sysext/extbase/Classes/Mvc/Request.php
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-94428-ExtbaseRequestImplementsServerRequestInterface.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-94428-ExtbaseRequestImplementsServerRequestInterface.rst
new file mode 100644
index 0000000000000000000000000000000000000000..2d366c840e60416c0d49bdf40d16dccbe29daef0
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-94428-ExtbaseRequestImplementsServerRequestInterface.rst
@@ -0,0 +1,40 @@
+.. include:: ../../Includes.txt
+
+===================================================================
+Feature: #94428 - Extbase Request implements ServerRequestInterface
+===================================================================
+
+See :issue:`94428`
+
+Description
+===========
+
+The extbase :php:`TYPO3\CMS\Extbase\Mvc\Request` now implements
+the PSR-7 :php:`ServerRequestInterface` and thus holds all request
+related information of the main core request in addition to the
+plugin namespace specific extbase arguments.
+
+
+Impact
+======
+
+This allows getting information of the main request especially within
+extbase controllers from :php:`$this->request`.
+
+Developers of fluid view helpers can now retrieve the main PSR-7 request
+in many contexts from :php:`$renderingContext->getRequest()`, in addition
+to the extbase specific information specified by
+:php:`TYPO3\CMS\Extbase\Mvc\Request\RequestInterface`.
+
+Note that with future patches, the request assigned to view helper
+:php:`RenderingContext` may NOT implement extbase
+:php:`TYPO3\CMS\Extbase\Mvc\Request\RequestInterface` anymore, and
+only PSR-7 :php:`ServerRequestInterface`. This will be the case when the
+view helper is not called from within an extbase plugin, but when fluid
+is started as "standalone view" in non-extbase based plugins: Often in
+backend scenarios like toolbars, doc headers, non-extbase modules, etc.
+Extensions should thus test for instance of extbase :php:`RequestInterface`
+if they don't know the context and rely on extbase specific request data.
+
+
+.. index:: PHP-API, ext:extbase
diff --git a/typo3/sysext/extbase/Classes/Mvc/ExtbaseRequestParameters.php b/typo3/sysext/extbase/Classes/Mvc/ExtbaseRequestParameters.php
new file mode 100644
index 0000000000000000000000000000000000000000..be6ab34ceecb6880e8a2a23ebbc7506089aeadcd
--- /dev/null
+++ b/typo3/sysext/extbase/Classes/Mvc/ExtbaseRequestParameters.php
@@ -0,0 +1,483 @@
+<?php
+
+/*
+ * 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\Extbase\Mvc;
+
+use TYPO3\CMS\Core\Utility\ClassNamingUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Error\Result;
+use TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException;
+use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException;
+use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException;
+use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
+
+/**
+ * Extbase request related state.
+ * Attached as 'extbase' attribute to PSR-7 ServerRequestInterface.
+ *
+ * @internal Set up extbase internally, use TYPO3\CMS\Extbase\Mvc\Request instead.
+ */
+class ExtbaseRequestParameters
+{
+    /**
+     * @var string Key of the plugin which identifies the plugin. It must be a string containing [a-z0-9]
+     */
+    protected $pluginName = '';
+
+    /**
+     * @var string Name of the extension which is supposed to handle this request. This is the extension name converted to UpperCamelCase
+     *
+     * @todo: Should probably at least init to empty string.
+     */
+    protected $controllerExtensionName;
+
+    /**
+     * @var string
+     *
+     * @todo: Should probably at least init to empty string.
+     */
+    protected $controllerObjectName;
+
+    /**
+     * @var string Object name of the controller which is supposed to handle this request.
+     */
+    protected $controllerName = 'Standard';
+
+    /**
+     * @var string Name of the action the controller is supposed to take.
+     */
+    protected $controllerActionName = 'index';
+
+    /**
+     * @var array The arguments for this request
+     */
+    protected $arguments = [];
+
+    /**
+     * Framework-internal arguments for this request, such as __referrer.
+     * All framework-internal arguments start with double underscore (__),
+     * and are only used from within the framework. Not for user consumption.
+     * Internal Arguments can be objects, in contrast to public arguments
+     *
+     * @var array
+     */
+    protected array $internalArguments = [];
+
+    /**
+     * @var string The requested representation format
+     */
+    protected $format = 'html';
+
+    /**
+     * @var bool If this request has been changed and needs to be dispatched again
+     * @deprecated since v11, will be removed in v12.
+     */
+    protected $dispatched = false;
+
+    /**
+     * If this request is a forward because of an error, the original request gets filled.
+     *
+     * @var \TYPO3\CMS\Extbase\Mvc\Request|null
+     */
+    protected $originalRequest;
+
+    /**
+     * If the request is a forward because of an error, these mapping results get filled here.
+     *
+     * @var \TYPO3\CMS\Extbase\Error\Result|null
+     */
+    protected $originalRequestMappingResults;
+
+    /**
+     * Sets the dispatched flag
+     *
+     * @param bool $flag If this request has been dispatched
+     * @deprecated since v11, will be removed in v12.
+     */
+    public function setDispatched($flag)
+    {
+        $this->dispatched = (bool)$flag;
+    }
+
+    /**
+     * If this request has been dispatched and addressed by the responsible
+     * controller and the response is ready to be sent.
+     *
+     * The dispatcher will try to dispatch the request again if it has not been
+     * addressed yet.
+     *
+     * @return bool TRUE if this request has been dispatched successfully
+     * @deprecated since v11, will be removed in v12.
+     */
+    public function isDispatched()
+    {
+        return $this->dispatched;
+    }
+
+    /**
+     * @param string $controllerClassName
+     */
+    public function __construct(string $controllerClassName = '')
+    {
+        $this->controllerObjectName = $controllerClassName;
+    }
+
+    /**
+     * @return string
+     */
+    public function getControllerObjectName(): string
+    {
+        return $this->controllerObjectName;
+    }
+
+    /**
+     * Explicitly sets the object name of the controller
+     *
+     * @param string $controllerObjectName The fully qualified controller object name
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setControllerObjectName($controllerObjectName)
+    {
+        $nameParts = ClassNamingUtility::explodeObjectControllerName($controllerObjectName);
+        $this->controllerExtensionName = $nameParts['extensionName'];
+        $this->controllerName = $nameParts['controllerName'];
+        return $this;
+    }
+
+    /**
+     * Sets the plugin name.
+     *
+     * @param string|null $pluginName
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setPluginName($pluginName = null)
+    {
+        if ($pluginName !== null) {
+            $this->pluginName = $pluginName;
+        }
+        return $this;
+    }
+
+    /**
+     * Returns the plugin key.
+     *
+     * @return string The plugin key
+     */
+    public function getPluginName()
+    {
+        return $this->pluginName;
+    }
+
+    /**
+     * Sets the extension name of the controller.
+     *
+     * @param string $controllerExtensionName The extension name.
+     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException if the extension name is not valid
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setControllerExtensionName($controllerExtensionName): self
+    {
+        if ($controllerExtensionName !== null) {
+            $this->controllerExtensionName = $controllerExtensionName;
+        }
+        return $this;
+    }
+
+    /**
+     * Returns the extension name of the specified controller.
+     *
+     * @return string The extension name
+     */
+    public function getControllerExtensionName()
+    {
+        return $this->controllerExtensionName;
+    }
+
+    /**
+     * Returns the extension name of the specified controller.
+     *
+     * @return string The extension key
+     */
+    public function getControllerExtensionKey()
+    {
+        return GeneralUtility::camelCaseToLowerCaseUnderscored($this->controllerExtensionName);
+    }
+
+    /**
+     * @var array
+     */
+    protected $controllerAliasToClassNameMapping = [];
+
+    /**
+     * @param array $controllerAliasToClassNameMapping
+     */
+    public function setControllerAliasToClassNameMapping(array $controllerAliasToClassNameMapping)
+    {
+        // this is only needed as long as forwarded requests are altered and unless there
+        // is no new request object created by the request builder.
+        $this->controllerAliasToClassNameMapping = $controllerAliasToClassNameMapping;
+        return $this;
+    }
+
+    /**
+     * Sets the name of the controller which is supposed to handle the request.
+     * Note: This is not the object name of the controller!
+     *
+     * @param string $controllerName Name of the controller
+     * @throws Exception\InvalidControllerNameException
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setControllerName($controllerName): self
+    {
+        if (!is_string($controllerName) && $controllerName !== null) {
+            throw new InvalidControllerNameException('The controller name must be a valid string, ' . gettype($controllerName) . ' given.', 1187176358);
+        }
+        if ($controllerName !== null) {
+            $this->controllerName = $controllerName;
+            $this->controllerObjectName = $this->controllerAliasToClassNameMapping[$controllerName] ?? '';
+            // There might be no Controller Class, for example for Fluid Templates.
+        }
+        return $this;
+    }
+
+    /**
+     * Returns the object name of the controller supposed to handle this request, if one
+     * was set already (if not, the name of the default controller is returned)
+     *
+     * @return string Object name of the controller
+     */
+    public function getControllerName()
+    {
+        return $this->controllerName;
+    }
+
+    /**
+     * Sets the name of the action contained in this request.
+     *
+     * Note that the action name must start with a lower case letter and is case sensitive.
+     *
+     * @param string $actionName Name of the action to execute by the controller
+     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException if the action name is not valid
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setControllerActionName($actionName): self
+    {
+        if (!is_string($actionName) && $actionName !== null) {
+            throw new InvalidActionNameException('The action name must be a valid string, ' . gettype($actionName) . ' given (' . $actionName . ').', 1187176359);
+        }
+        if ($actionName[0] !== strtolower($actionName[0]) && $actionName !== null) {
+            throw new InvalidActionNameException('The action name must start with a lower case letter, "' . $actionName . '" does not match this criteria.', 1218473352);
+        }
+        if ($actionName !== null) {
+            $this->controllerActionName = $actionName;
+        }
+        return $this;
+    }
+
+    /**
+     * Returns the name of the action the controller is supposed to execute.
+     *
+     * @return string Action name
+     */
+    public function getControllerActionName(): string
+    {
+        $controllerObjectName = $this->getControllerObjectName();
+        if ($controllerObjectName !== '' && $this->controllerActionName === strtolower($this->controllerActionName)) {
+            // todo: this is nonsense! We can detect a non existing method in
+            // todo: \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin, if necessary.
+            // todo: At this point, we want to have a getter for a fixed value.
+            $actionMethodName = $this->controllerActionName . 'Action';
+            $classMethods = get_class_methods($controllerObjectName);
+            if (is_array($classMethods)) {
+                foreach ($classMethods as $existingMethodName) {
+                    if (strtolower($existingMethodName) === strtolower($actionMethodName)) {
+                        $this->controllerActionName = substr($existingMethodName, 0, -6);
+                        break;
+                    }
+                }
+            }
+        }
+        return $this->controllerActionName;
+    }
+
+    /**
+     * Sets the value of the specified argument
+     *
+     * @param string $argumentName Name of the argument to set
+     * @param mixed $value The new value
+     * @throws InvalidArgumentNameException
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setArgument(string $argumentName, $value): self
+    {
+        if ($argumentName === '') {
+            throw new InvalidArgumentNameException('Invalid argument name.', 1210858767);
+        }
+        if ($argumentName[0] === '_' && $argumentName[1] === '_') {
+            $this->internalArguments[$argumentName] = $value;
+            return $this;
+        }
+        if (!in_array($argumentName, ['@extension', '@subpackage', '@controller', '@action', '@format'], true)) {
+            $this->arguments[$argumentName] = $value;
+        }
+        return $this;
+    }
+
+    /**
+     * Sets the whole arguments array and therefore replaces any arguments
+     * which existed before.
+     *
+     * @param array $arguments An array of argument names and their values
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setArguments(array $arguments): self
+    {
+        $this->arguments = [];
+        foreach ($arguments as $argumentName => $argumentValue) {
+            $this->setArgument($argumentName, $argumentValue);
+        }
+        return $this;
+    }
+
+    /**
+     * Returns an array of arguments and their values
+     *
+     * @return array Associative array of arguments and their values (which may be arguments and values as well)
+     */
+    public function getArguments()
+    {
+        return $this->arguments;
+    }
+
+    /**
+     * Returns the value of the specified argument
+     *
+     * @param string $argumentName Name of the argument
+     *
+     * @return string|array Value of the argument
+     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException if such an argument does not exist
+     */
+    public function getArgument($argumentName)
+    {
+        if (!isset($this->arguments[$argumentName])) {
+            throw new NoSuchArgumentException('An argument "' . $argumentName . '" does not exist for this request.', 1176558158);
+        }
+        return $this->arguments[$argumentName];
+    }
+
+    /**
+     * Checks if an argument of the given name exists (is set)
+     *
+     * @param string $argumentName Name of the argument to check
+     *
+     * @return bool TRUE if the argument is set, otherwise FALSE
+     */
+    public function hasArgument($argumentName)
+    {
+        return isset($this->arguments[$argumentName]);
+    }
+
+    /**
+     * Sets the requested representation format
+     *
+     * @param string $format The desired format, something like "html", "xml", "png", "json" or the like. Can even be something like "rss.xml".
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setFormat(string $format): self
+    {
+        $this->format = $format;
+        return $this;
+    }
+
+    /**
+     * Returns the requested representation format
+     *
+     * @return string The desired format, something like "html", "xml", "png", "json" or the like.
+     */
+    public function getFormat()
+    {
+        return $this->format;
+    }
+
+    /**
+     * Returns the original request. Filled only if a property mapping error occurred.
+     *
+     * @return \TYPO3\CMS\Extbase\Mvc\Request|null the original request.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function getOriginalRequest(): ?Request
+    {
+        return $this->originalRequest;
+    }
+
+    /**
+     * @param \TYPO3\CMS\Extbase\Mvc\Request $originalRequest
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setOriginalRequest(\TYPO3\CMS\Extbase\Mvc\Request $originalRequest)
+    {
+        $this->originalRequest = $originalRequest;
+    }
+
+    /**
+     * Get the request mapping results for the original request.
+     *
+     * @return \TYPO3\CMS\Extbase\Error\Result
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function getOriginalRequestMappingResults(): Result
+    {
+        if ($this->originalRequestMappingResults === null) {
+            return new Result();
+        }
+        return $this->originalRequestMappingResults;
+    }
+
+    /**
+     * @param \TYPO3\CMS\Extbase\Error\Result $originalRequestMappingResults
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function setOriginalRequestMappingResults(Result $originalRequestMappingResults)
+    {
+        $this->originalRequestMappingResults = $originalRequestMappingResults;
+    }
+
+    /**
+     * Get the internal arguments of the request, i.e. every argument starting
+     * with two underscores.
+     *
+     * @return array
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function getInternalArguments(): array
+    {
+        return $this->internalArguments;
+    }
+
+    /**
+     * Returns the value of the specified argument
+     *
+     * @param string $argumentName Name of the argument
+     * @return string|null Value of the argument, or NULL if not set.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     */
+    public function getInternalArgument($argumentName)
+    {
+        if (!isset($this->internalArguments[$argumentName])) {
+            return null;
+        }
+        return $this->internalArguments[$argumentName];
+    }
+}
diff --git a/typo3/sysext/extbase/Classes/Mvc/Request.php b/typo3/sysext/extbase/Classes/Mvc/Request.php
index ee0cadf3fa614fa45226b6d60381d643cbe17a87..18273f500e8183a496041a6056920fca272604c8 100644
--- a/typo3/sysext/extbase/Classes/Mvc/Request.php
+++ b/typo3/sysext/extbase/Classes/Mvc/Request.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 /*
  * This file is part of the TYPO3 CMS project.
  *
@@ -15,507 +17,722 @@
 
 namespace TYPO3\CMS\Extbase\Mvc;
 
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriInterface;
 use TYPO3\CMS\Core\Http\ApplicationType;
 use TYPO3\CMS\Core\Http\NormalizedParams;
-use TYPO3\CMS\Core\Utility\ClassNamingUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Extbase\Error\Result;
-use TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException;
-use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException;
-use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException;
-use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
 
 /**
- * Represents a generic request.
+ * The extbase request.
+ *
+ * This is a decorator: The core PSR-7 request is hand over as constructor
+ * argument, this class implements ServerRequestInterface, too.
+ * Additionally, the extbase request details are attached as 'extbase'
+ * attribute to the PSR-7 request and this class implements extbase RequestInterface.
+ * This class has no state except the PSR-7 request, all operations are
+ * hand down to the PSR-7 request.
  */
-class Request implements RequestInterface
+class Request implements ServerRequestInterface, RequestInterface
 {
-    const PATTERN_MATCH_FORMAT = '/^[a-z0-9]{1,5}$/';
+    protected ServerRequestInterface $request;
 
     /**
-     * @var string Key of the plugin which identifies the plugin. It must be a string containing [a-z0-9]
+     * @todo v12: final public function __construct(ServerRequestInterface $request)
      */
-    protected $pluginName = '';
+    public function __construct($request = null)
+    {
+        if (is_string($request) && !empty($request)) {
+            // Deprecation layer for old extbase Request __construct(string $controllerClassName = '')
+            $controllerClassName = $request;
+            /** @var ServerRequestInterface $request */
+            $request = $GLOBALS['TYPO3_REQUEST'] ?? new ServerRequest();
+            $attribute = new ExtbaseRequestParameters($controllerClassName);
+            $request = $request->withAttribute('extbase', $attribute);
+        } elseif ($request === null) {
+            // Deprecation layer when ServerRequestInterface is not given yet
+            /** @var ServerRequestInterface $request */
+            // Fallback "new ServerRequest()" currently used in install tool.
+            $request = $GLOBALS['TYPO3_REQUEST'] ?? new ServerRequest();
+            $attribute = new ExtbaseRequestParameters('');
+            $request = $request->withAttribute('extbase', $attribute);
+        }
+        if (!$request instanceof ServerRequestInterface) {
+            throw new \InvalidArgumentException(
+                'Request must implement PSR-7 ServerRequestInterface',
+                1624452071
+            );
+        }
+        if (!$request->getAttribute('extbase') instanceof ExtbaseRequestParameters) {
+            throw new \InvalidArgumentException(
+                'Given request must have an attribute "extbase" of type ExtbaseAttribute',
+                1624452070
+            );
+        }
+        $this->request = $request;
+    }
 
     /**
-     * @var string Name of the extension which is supposed to handle this request. This is the extension name converted to UpperCamelCase
+     * ExtbaseAttribute attached as attribute 'extbase' to $request carries extbase
+     * specific request values. This helper method type hints this attribute.
      */
-    protected $controllerExtensionName;
+    protected function getExtbaseAttribute(): ExtbaseRequestParameters
+    {
+        return $this->request->getAttribute('extbase');
+    }
 
-    /**
-     * @var string
-     */
-    protected $controllerObjectName;
+    public function getServerRequest(): ServerRequestInterface
+    {
+        return $this->request;
+    }
 
     /**
-     * @var string Object name of the controller which is supposed to handle this request.
+     * Methods implementing extbase RequestInterface
      */
-    protected $controllerName = 'Standard';
 
     /**
-     * @var string Name of the action the controller is supposed to take.
+     * @inheritdoc
      */
-    protected $controllerActionName = 'index';
+    public function getControllerObjectName(): string
+    {
+        return $this->getExtbaseAttribute()->getControllerObjectName();
+    }
 
     /**
-     * @var array The arguments for this request
+     * Return an instance with the specified controller object name set.
      */
-    protected $arguments = [];
+    public function withControllerObjectName(string $controllerObjectName): self
+    {
+        $attribute = $this->getExtbaseAttribute()->setControllerObjectName($controllerObjectName);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
+    }
 
     /**
-     * Framework-internal arguments for this request, such as __referrer.
-     * All framework-internal arguments start with double underscore (__),
-     * and are only used from within the framework. Not for user consumption.
-     * Internal Arguments can be objects, in contrast to public arguments
+     * Returns the plugin key.
      *
-     * @var array
-     */
-    protected $internalArguments = [];
-
-    /**
-     * @var string The requested representation format
+     * @todo: Should be "public function getPluginName(): string", blocked by testing-framework
      */
-    protected $format = 'html';
+    public function getPluginName()
+    {
+        return $this->getExtbaseAttribute()->getPluginName();
+    }
 
     /**
-     * @var bool If this request has been changed and needs to be dispatched again
-     * @deprecated since v11, will be removed in v12.
+     * Return an instance with the specified plugin name set.
      */
-    protected $dispatched = false;
+    public function withPluginName($pluginName = null): self
+    {
+        $attribute = $this->getExtbaseAttribute()->setPluginName($pluginName);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
+    }
 
     /**
-     * If this request is a forward because of an error, the original request gets filled.
+     * Returns the extension name of the specified controller.
      *
-     * @var \TYPO3\CMS\Extbase\Mvc\Request|null
+     * @todo: Should be "public function getControllerExtensionName(): string", blocked by testing-framework
      */
-    protected $originalRequest;
+    public function getControllerExtensionName()
+    {
+        return $this->getExtbaseAttribute()->getControllerExtensionName();
+    }
 
     /**
-     * If the request is a forward because of an error, these mapping results get filled here.
+     * Return an instance with the specified controller extension name set.
      *
-     * @var \TYPO3\CMS\Extbase\Error\Result|null
+     * @param string|null $controllerExtensionName Extension name
+     * @return self
      */
-    protected $originalRequestMappingResults;
+    public function withControllerExtensionName($controllerExtensionName): self
+    {
+        $attribute = $this->getExtbaseAttribute()->setControllerExtensionName($controllerExtensionName);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
+    }
 
     /**
-     * Sets the dispatched flag
+     * Returns the extension key of the specified controller.
      *
-     * @param bool $flag If this request has been dispatched
-     * @deprecated since v11, will be removed in v12.
+     * @return string The extension key
      */
-    public function setDispatched($flag)
+    public function getControllerExtensionKey(): string
     {
-        $this->dispatched = (bool)$flag;
+        return $this->getExtbaseAttribute()->getControllerExtensionKey();
     }
 
     /**
-     * If this request has been dispatched and addressed by the responsible
-     * controller and the response is ready to be sent.
-     *
-     * The dispatcher will try to dispatch the request again if it has not been
-     * addressed yet.
+     * Returns the controller name supposed to handle this request, if one
+     * was set already (if not, the name of the default controller is returned)
      *
-     * @return bool TRUE if this request has been dispatched successfully
-     * @deprecated since v11, will be removed in v12.
+     * @todo: Should be "public function getControllerName(): string", blocked by testing-framework
      */
-    public function isDispatched()
+    public function getControllerName()
     {
-        return $this->dispatched;
+        return (string)$this->getExtbaseAttribute()->getControllerName();
     }
 
     /**
-     * @param string $controllerClassName
+     * Return an instance with the specified controller name set.
+     * Note: This is not the object name of the controller!
+     *
+     * @param string|null $controllerName Controller name
+     * @return self
      */
-    public function __construct(string $controllerClassName = '')
+    public function withControllerName($controllerName): self
     {
-        $this->controllerObjectName = $controllerClassName;
+        $attribute = $this->getExtbaseAttribute()->setControllerName($controllerName);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
     }
 
     /**
-     * @return string
+     * Returns the name of the action the controller is supposed to execute.
+     *
+     * @todo: Should be "public function getControllerActionName(): string", blocked by testing-framework
      */
-    public function getControllerObjectName(): string
+    public function getControllerActionName()
     {
-        return $this->controllerObjectName;
+        return $this->getExtbaseAttribute()->getControllerActionName();
     }
 
     /**
-     * Explicitly sets the object name of the controller
+     * Return an instance with the specified controller action name set.
      *
-     * @param string $controllerObjectName The fully qualified controller object name
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * Note that the action name must start with a lower case letter and is case sensitive.
+     *
+     * @param string|null $actionName Action name
+     * @return self
      */
-    public function setControllerObjectName($controllerObjectName)
+    public function withControllerActionName($actionName): self
     {
-        $nameParts = ClassNamingUtility::explodeObjectControllerName($controllerObjectName);
-        $this->controllerExtensionName = $nameParts['extensionName'];
-        $this->controllerName = $nameParts['controllerName'];
+        $attribute = $this->getExtbaseAttribute()->setControllerActionName($actionName);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
     }
 
     /**
-     * Sets the plugin name.
-     *
-     * @param string|null $pluginName
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * @inheritdoc
      */
-    public function setPluginName($pluginName = null)
+    public function getArguments(): array
     {
-        if ($pluginName !== null) {
-            $this->pluginName = $pluginName;
-        }
+        return $this->getExtbaseAttribute()->getArguments();
     }
 
     /**
-     * Returns the plugin key.
-     *
-     * @return string The plugin key
+     * Return an instance with the specified extbase arguments, replacing
+     * any arguments which existed before.
      */
-    public function getPluginName()
+    public function withArguments(array $arguments): self
     {
-        return $this->pluginName;
+        $attribute = $this->getExtbaseAttribute()->setArguments($arguments);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
     }
 
     /**
-     * Sets the extension name of the controller.
-     *
-     * @param string $controllerExtensionName The extension name.
-     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException if the extension name is not valid
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * @inheritdoc
      */
-    public function setControllerExtensionName($controllerExtensionName)
+    public function getArgument($argumentName)
     {
-        if ($controllerExtensionName !== null) {
-            $this->controllerExtensionName = $controllerExtensionName;
-        }
+        return $this->getExtbaseAttribute()->getArgument($argumentName);
     }
 
     /**
-     * Returns the extension name of the specified controller.
-     *
-     * @return string The extension name
+     * @inheritDoc
      */
-    public function getControllerExtensionName()
+    public function hasArgument($argumentName): bool
     {
-        return $this->controllerExtensionName;
+        return $this->getExtbaseAttribute()->hasArgument($argumentName);
     }
 
     /**
-     * Returns the extension name of the specified controller.
+     * Return an instance with the specified argument set.
      *
-     * @return string The extension key
+     * @param string $argumentName Name of the argument to set
+     * @param mixed $value The new value
+     * @return self
      */
-    public function getControllerExtensionKey()
+    public function withArgument(string $argumentName, $value): self
     {
-        return GeneralUtility::camelCaseToLowerCaseUnderscored($this->controllerExtensionName);
+        $attribute = $this->getExtbaseAttribute()->setArgument($argumentName, $value);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
     }
 
     /**
-     * @var array
+     * Returns the requested representation format, something
+     * like "html", "xml", "png", "json" or the like.
      */
-    protected $controllerAliasToClassNameMapping = [];
-
-    /**
-     * @param array $controllerAliasToClassNameMapping
-     */
-    public function setControllerAliasToClassNameMapping(array $controllerAliasToClassNameMapping)
+    public function getFormat(): string
     {
-        // this is only needed as long as forwarded requests are altered and unless there
-        // is no new request object created by the request builder.
-        $this->controllerAliasToClassNameMapping = $controllerAliasToClassNameMapping;
+        return $this->getExtbaseAttribute()->getFormat();
     }
 
     /**
-     * Sets the name of the controller which is supposed to handle the request.
-     * Note: This is not the object name of the controller!
+     * Return an instance with the specified derived request attribute.
      *
-     * @param string $controllerName Name of the controller
-     * @throws Exception\InvalidControllerNameException
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * This method allows setting a single derived request attribute as
+     * described in getFormat().
      */
-    public function setControllerName($controllerName)
+    public function withFormat(string $format): self
     {
-        if (!is_string($controllerName) && $controllerName !== null) {
-            throw new InvalidControllerNameException('The controller name must be a valid string, ' . gettype($controllerName) . ' given.', 1187176358);
-        }
-        if ($controllerName !== null) {
-            $this->controllerName = $controllerName;
-            $this->controllerObjectName = $this->controllerAliasToClassNameMapping[$controllerName] ?? '';
-            // There might be no Controller Class, for example for Fluid Templates.
-        }
+        $attribute = $this->getExtbaseAttribute()->setFormat($format);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
     }
 
     /**
-     * Returns the object name of the controller supposed to handle this request, if one
-     * was set already (if not, the name of the default controller is returned)
-     *
-     * @return string Object name of the controller
+     * Extbase @internal methods, not part of extbase RequestInterface. Should vanish as soon as unused.
      */
-    public function getControllerName()
+
+    /**
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
+     */
+    public function setControllerObjectName($controllerObjectName)
     {
-        return $this->controllerName;
+        $this->getExtbaseAttribute()->setControllerObjectName($controllerObjectName);
     }
 
     /**
-     * Sets the name of the action contained in this request.
-     *
-     * Note that the action name must start with a lower case letter and is case sensitive.
-     *
-     * @param string $actionName Name of the action to execute by the controller
-     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException if the action name is not valid
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function setControllerActionName($actionName)
+    public function setPluginName($pluginName = null)
     {
-        if (!is_string($actionName) && $actionName !== null) {
-            throw new InvalidActionNameException('The action name must be a valid string, ' . gettype($actionName) . ' given (' . $actionName . ').', 1187176359);
-        }
-        if ($actionName[0] !== strtolower($actionName[0]) && $actionName !== null) {
-            throw new InvalidActionNameException('The action name must start with a lower case letter, "' . $actionName . '" does not match this criteria.', 1218473352);
-        }
-        if ($actionName !== null) {
-            $this->controllerActionName = $actionName;
-        }
+        $this->getExtbaseAttribute()->setPluginName($pluginName);
     }
 
     /**
-     * Returns the name of the action the controller is supposed to execute.
-     *
-     * @return string Action name
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function getControllerActionName()
+    public function setControllerExtensionName($controllerExtensionName)
     {
-        $controllerObjectName = $this->getControllerObjectName();
-        if ($controllerObjectName !== '' && $this->controllerActionName === strtolower($this->controllerActionName)) {
-            // todo: this is nonsense! We can detect a non existing method in
-            // todo: \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin, if necessary.
-            // todo: At this point, we want to have a getter for a fixed value.
-            $actionMethodName = $this->controllerActionName . 'Action';
-            $classMethods = get_class_methods($controllerObjectName);
-            if (is_array($classMethods)) {
-                foreach ($classMethods as $existingMethodName) {
-                    if (strtolower($existingMethodName) === strtolower($actionMethodName)) {
-                        $this->controllerActionName = substr($existingMethodName, 0, -6);
-                        break;
-                    }
-                }
-            }
-        }
-        return $this->controllerActionName;
+        $this->getExtbaseAttribute()->setControllerExtensionName($controllerExtensionName);
     }
 
     /**
-     * Sets the value of the specified argument
-     *
-     * @param string $argumentName Name of the argument to set
-     * @param mixed $value The new value
-     * @throws Exception\InvalidArgumentNameException
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function setArgument($argumentName, $value)
+    public function setControllerAliasToClassNameMapping(array $controllerAliasToClassNameMapping)
     {
-        if (!is_string($argumentName) || $argumentName === '') {
-            throw new InvalidArgumentNameException('Invalid argument name.', 1210858767);
-        }
-        if ($argumentName[0] === '_' && $argumentName[1] === '_') {
-            $this->internalArguments[$argumentName] = $value;
-            return;
-        }
-        if (!in_array($argumentName, ['@extension', '@subpackage', '@controller', '@action', '@format'], true)) {
-            $this->arguments[$argumentName] = $value;
-        }
+        // this is only needed as long as forwarded requests are altered and unless there
+        // is no new request object created by the request builder.
+        $this->getExtbaseAttribute()->setControllerAliasToClassNameMapping($controllerAliasToClassNameMapping);
     }
 
     /**
-     * Sets the whole arguments array and therefore replaces any arguments
-     * which existed before.
-     *
-     * @param array $arguments An array of argument names and their values
      * @internal only to be used within Extbase, not part of TYPO3 Core API.
      */
-    public function setArguments(array $arguments)
+    public function withControllerAliasToClassNameMapping(array $controllerAliasToClassNameMapping): self
     {
-        $this->arguments = [];
-        foreach ($arguments as $argumentName => $argumentValue) {
-            $this->setArgument($argumentName, $argumentValue);
-        }
+        // this is only needed as long as forwarded requests are altered and unless there
+        // is no new request object created by the request builder.
+        $attribute = $this->getExtbaseAttribute()->setControllerAliasToClassNameMapping($controllerAliasToClassNameMapping);
+        $request = $this->request->withAttribute('extbase', $attribute);
+        return new static($request);
     }
 
     /**
-     * Returns an array of arguments and their values
-     *
-     * @return array Associative array of arguments and their values (which may be arguments and values as well)
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function getArguments()
+    public function setControllerName($controllerName)
     {
-        return $this->arguments;
+        $this->getExtbaseAttribute()->setControllerName($controllerName);
     }
 
     /**
-     * Returns the value of the specified argument
-     *
-     * @param string $argumentName Name of the argument
-     *
-     * @return string|array Value of the argument
-     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException if such an argument does not exist
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function getArgument($argumentName)
+    public function setControllerActionName($actionName)
     {
-        if (!isset($this->arguments[$argumentName])) {
-            throw new NoSuchArgumentException('An argument "' . $argumentName . '" does not exist for this request.', 1176558158);
-        }
-        return $this->arguments[$argumentName];
+        $this->getExtbaseAttribute()->setControllerActionName($actionName);
     }
 
     /**
-     * Checks if an argument of the given name exists (is set)
-     *
-     * @param string $argumentName Name of the argument to check
-     *
-     * @return bool TRUE if the argument is set, otherwise FALSE
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function hasArgument($argumentName)
+    public function setArgument($argumentName, $value)
     {
-        return isset($this->arguments[$argumentName]);
+        $this->getExtbaseAttribute()->setArgument($argumentName, $value);
     }
 
     /**
-     * Sets the requested representation format
-     *
-     * @param string $format The desired format, something like "html", "xml", "png", "json" or the like. Can even be something like "rss.xml".
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function setFormat($format)
+    public function setArguments(array $arguments)
     {
-        $this->format = $format;
+        $this->getExtbaseAttribute()->setArguments($arguments);
     }
 
     /**
-     * Returns the requested representation format
-     *
-     * @return string The desired format, something like "html", "xml", "png", "json" or the like.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function getFormat()
+    public function setFormat($format)
     {
-        return $this->format;
+        $this->getExtbaseAttribute()->setFormat($format);
     }
 
     /**
-     * Returns the original request. Filled only if a property mapping error occurred.
-     *
-     * @return \TYPO3\CMS\Extbase\Mvc\Request|null the original request.
      * @internal only to be used within Extbase, not part of TYPO3 Core API.
      */
     public function getOriginalRequest(): ?Request
     {
-        return $this->originalRequest;
+        return $this->getExtbaseAttribute()->getOriginalRequest();
     }
 
     /**
-     * @param \TYPO3\CMS\Extbase\Mvc\Request $originalRequest
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
-    public function setOriginalRequest(\TYPO3\CMS\Extbase\Mvc\Request $originalRequest)
+    public function setOriginalRequest(Request $originalRequest)
     {
-        $this->originalRequest = $originalRequest;
+        $this->getExtbaseAttribute()->setOriginalRequest($originalRequest);
     }
 
     /**
      * Get the request mapping results for the original request.
      *
-     * @return \TYPO3\CMS\Extbase\Error\Result
      * @internal only to be used within Extbase, not part of TYPO3 Core API.
      */
     public function getOriginalRequestMappingResults(): Result
     {
-        if ($this->originalRequestMappingResults === null) {
-            return new Result();
-        }
-        return $this->originalRequestMappingResults;
+        return $this->getExtbaseAttribute()->getOriginalRequestMappingResults();
     }
 
     /**
-     * @param \TYPO3\CMS\Extbase\Error\Result $originalRequestMappingResults
-     * @internal only to be used within Extbase, not part of TYPO3 Core API.
+     * @internal only to be used within Extbase, not part of TYPO3 Core API. Violates immutability.
      */
     public function setOriginalRequestMappingResults(Result $originalRequestMappingResults)
     {
-        $this->originalRequestMappingResults = $originalRequestMappingResults;
+        $this->getExtbaseAttribute()->setOriginalRequestMappingResults($originalRequestMappingResults);
     }
 
     /**
-     * Get the internal arguments of the request, i.e. every argument starting
-     * with two underscores.
-     *
-     * @return array
      * @internal only to be used within Extbase, not part of TYPO3 Core API.
      */
-    public function getInternalArguments()
+    public function getInternalArguments(): array
     {
-        return $this->internalArguments;
+        return $this->getExtbaseAttribute()->getInternalArguments();
     }
 
     /**
-     * Returns the value of the specified argument
-     *
-     * @param string $argumentName Name of the argument
-     * @return string Value of the argument, or NULL if not set.
      * @internal only to be used within Extbase, not part of TYPO3 Core API.
      */
     public function getInternalArgument($argumentName)
     {
-        if (!isset($this->internalArguments[$argumentName])) {
-            return null;
-        }
-        return $this->internalArguments[$argumentName];
+        return $this->getExtbaseAttribute()->getInternalArgument($argumentName);
     }
 
     /**
-     * Returns the name of the request method
-     *
-     * @return string Name of the request method
+     * Deprecated methods of extbase Request for v11 compat.
+     */
+
+    /**
+     * @deprecated since v11, will be removed in v12. Violates immutability.
      */
-    public function getMethod()
+    public function setDispatched($flag)
     {
-        // @todo Global access is obsolete as soon as this class implements ServerRequestInterface
-        $request = $GLOBALS['TYPO3_REQUEST'];
-        return $request->getMethod();
+        $this->getExtbaseAttribute()->setDispatched($flag);
     }
 
     /**
-     * Returns the request URI
-     *
-     * @return string URI of this web request
-     * @deprecated since v11, will be removed in v12
+     * @deprecated since v11, will be removed in v12.
+     */
+    public function isDispatched()
+    {
+        return $this->getExtbaseAttribute()->isDispatched();
+    }
+
+    /**
+     * @deprecated since v11, will be removed in v12.
      */
     public function getRequestUri()
     {
         trigger_error('Method ' . __METHOD__ . ' is deprecated and will be removed in TYPO3 12.0', E_USER_DEPRECATED);
-
-        // @todo Global access is obsolete as soon as this class implements ServerRequestInterface
-        $mainRequest = $GLOBALS['TYPO3_REQUEST'];
         /** @var NormalizedParams $normalizedParams */
-        $normalizedParams = $mainRequest->getAttribute('normalizedParams');
+        $normalizedParams = $this->getAttribute('normalizedParams');
         return $normalizedParams->getRequestUrl();
     }
 
     /**
-     * Returns the base URI
-     *
-     * @return string Base URI of this web request
-     * @deprecated since v11, will be removed in v12
+     * @deprecated since v11, will be removed in v12.
      */
     public function getBaseUri()
     {
         trigger_error('Method ' . __METHOD__ . ' is deprecated and will be removed in TYPO3 12.0', E_USER_DEPRECATED);
-
-        // @todo Global access is obsolete as soon as this class implements ServerRequestInterface
-        $mainRequest = $GLOBALS['TYPO3_REQUEST'];
         /** @var NormalizedParams $normalizedParams */
-        $normalizedParams = $mainRequest->getAttribute('normalizedParams');
+        $normalizedParams = $this->getAttribute('normalizedParams');
         $baseUri = $normalizedParams->getSiteUrl();
-        if (ApplicationType::fromRequest($mainRequest)->isBackend()) {
+        if (ApplicationType::fromRequest($this)->isBackend()) {
             $baseUri .= TYPO3_mainDir;
         }
         return $baseUri;
     }
+
+    /**
+     * Methods implementing ServerRequestInterface
+     */
+
+    /**
+     * @inheritdoc
+     */
+    public function getServerParams(): array
+    {
+        return $this->request->getServerParams();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getCookieParams(): array
+    {
+        return $this->request->getCookieParams();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withCookieParams(array $cookies): self
+    {
+        $request = $this->request->withCookieParams($cookies);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getQueryParams(): array
+    {
+        return $this->request->getQueryParams();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withQueryParams(array $query): self
+    {
+        $request = $this->request->withQueryParams($query);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getUploadedFiles(): array
+    {
+        return $this->request->getUploadedFiles();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withUploadedFiles(array $uploadedFiles): self
+    {
+        $request = $this->request->withUploadedFiles($uploadedFiles);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getParsedBody()
+    {
+        return $this->request->getParsedBody();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withParsedBody($data): self
+    {
+        $request = $this->request->withParsedBody($data);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getAttributes(): array
+    {
+        return $this->request->getAttributes();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getAttribute($name, $default = null)
+    {
+        return $this->request->getAttribute($name, $default);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withAttribute($name, $value): self
+    {
+        $request = $this->request->withAttribute($name, $value);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withoutAttribute($name): self
+    {
+        $request = $this->request->withoutAttribute($name);
+        return new static($request);
+    }
+
+    /**
+     * Methods implementing RequestInterface
+     */
+
+    /**
+     * @inheritdoc
+     */
+    public function getRequestTarget(): string
+    {
+        return $this->request->getRequestTarget();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withRequestTarget($requestTarget): self
+    {
+        $request = $this->request->withRequestTarget($requestTarget);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getMethod(): string
+    {
+        return $this->request->getMethod();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withMethod($method): self
+    {
+        $request = $this->request->withMethod($method);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getUri(): UriInterface
+    {
+        return $this->request->getUri();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withUri(UriInterface $uri, $preserveHost = false): self
+    {
+        $request = $this->request->withUri($uri, $preserveHost);
+        return new static($request);
+    }
+
+    /**
+     * Methods implementing MessageInterface
+     */
+
+    /**
+     * @inheritdoc
+     */
+    public function getProtocolVersion(): string
+    {
+        return $this->request->getProtocolVersion();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withProtocolVersion($version): self
+    {
+        $request = $this->request->withProtocolVersion($version);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getHeaders(): array
+    {
+        return $this->request->getHeaders();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function hasHeader($name): bool
+    {
+        return $this->request->hasHeader($name);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getHeader($name): array
+    {
+        return $this->request->getHeader($name);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getHeaderLine($name): string
+    {
+        return $this->request->getHeaderLine($name);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withHeader($name, $value): self
+    {
+        $request = $this->request->withHeader($name, $value);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withAddedHeader($name, $value): self
+    {
+        $request = $this->request->withAddedHeader($name, $value);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withoutHeader($name): self
+    {
+        $request = $this->request->withoutHeader($name);
+        return new static($request);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getBody(): StreamInterface
+    {
+        return $this->request->getBody();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function withBody(StreamInterface $body): self
+    {
+        $request = $this->request->withBody($body);
+        return new static($request);
+    }
 }
diff --git a/typo3/sysext/extbase/Classes/Mvc/RequestInterface.php b/typo3/sysext/extbase/Classes/Mvc/RequestInterface.php
index b806a057c20adb32752465c72db3d8472a0d2cf7..d88b3709f9e9d2fbf561f777b99103fd859de273 100644
--- a/typo3/sysext/extbase/Classes/Mvc/RequestInterface.php
+++ b/typo3/sysext/extbase/Classes/Mvc/RequestInterface.php
@@ -15,77 +15,156 @@
 
 namespace TYPO3\CMS\Extbase\Mvc;
 
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
+
 /**
- * Contract for a request.
+ * Contract for an extbase request.
  */
 interface RequestInterface
 {
     /**
-     * Sets the dispatched flag
+     * Return the original PSR-7 request.
      *
-     * @param bool $flag If this request has been dispatched
-     * @deprecated since v11, will be removed in v12.
+     * @return ServerRequestInterface
+     * @todo v12: Enable
+     */
+    // public function getServerRequest(): ServerRequestInterface;
+
+    /**
+     * Returns the plugin key.
+     * @todo v12: Enable
      */
-    public function setDispatched($flag);
+    // public function getPluginName(): string;
 
     /**
-     * If this request has been dispatched and addressed by the responsible
-     * controller and the response is ready to be sent.
+     * Return an instance with the specified plugin name set.
      *
-     * The dispatcher will try to dispatch the request again if it has not been
-     * addressed yet.
+     * @param string|null Plugin name
+     * @return self
+     * @todo v12: Enable
+     */
+    // public function withPluginName($pluginName = null): self;
+
+    /**
+     * Returns the extension name of the specified controller.
      *
-     * @return bool TRUE if this request has been dispatched successfully
-     * @deprecated since v11, will be removed in v12.
+     * @return string|null
+     * @todo v12: Enable
      */
-    public function isDispatched();
+    // public function getControllerExtensionName(): ?string;
 
     /**
-     * Returns the object name of the controller defined by the package key and
-     * controller name
+     * Return an instance with the specified controller extension name set.
+     *
+     * @param string|null Extension name
+     * @return self
+     * @todo v12: Enable
+     */
+    // public function withControllerExtensionName($controllerExtensionName): RequestInterface;
+
+    /**
+     * Returns the extension key of the specified controller.
+     * @todo v12: Enable
+     */
+    // public function getControllerExtensionKey(): string;
+
+    /**
+     * Returns the object name of the controller defined by the package
+     * key and controller name.
      *
      * @return string The controller's Object Name
-     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchControllerException if the controller does not exist
+     * @todo v12: public function getControllerObjectName(): string
      */
     public function getControllerObjectName();
 
     /**
-     * Sets the value of the specified argument
-     *
-     * @param string $argumentName Name of the argument to set
-     * @param mixed $value The new value
+     * Return an instance with the specified controller object name set.
+     * @todo v12: Enable
+     */
+    // public function withControllerObjectName(string $controllerObjectName): RequestInterface;
+
+    /**
+     * Return an instance with the specified controller alias
+     * to class name mapping set.
+     * @todo v12: Enable or refactor to render it obsolete.
+     */
+    // public function withControllerAliasToClassNameMapping(array $controllerAliasToClassNameMapping): RequestInterface;
+
+    /**
+     * Returns the object name of the controller supposed to handle this request, if one
+     * was specified already (if not, the name of the default controller is returned)
+     * @todo v12: Enable
+     */
+    // public function getControllerName(): string;
+
+    /**
+     * Returns the name of the action the controller is supposed to execute.
+     * @todo v12: Enable
      */
-    public function setArgument($argumentName, $value);
+    // public function getControllerActionName(): string;
 
     /**
-     * Sets the whole arguments array and therefore replaces any arguments
-     * which existed before.
+     * Return an instance with the specified controller action name set.
      *
-     * @param array $arguments An array of argument names and their values
+     * Note that the action name must start with a lower case letter and is case sensitive.
+     * @param string|null Action name
+     * @return self
+     * @todo v12: Enable
      */
-    public function setArguments(array $arguments);
+    //public function withControllerActionName($actionName): RequestInterface;
 
     /**
-     * Returns the value of the specified argument
+     * Returns the value of the specified argument.
      *
      * @param string $argumentName Name of the argument
-     * @return string Value of the argument
-     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException if such an argument does not exist
+     * @return string|array Value of the argument
+     * @throws NoSuchArgumentException if such an argument does not exist
+     * @todo v12: public function getArgument(string $argumentName)
      */
     public function getArgument($argumentName);
 
     /**
-     * Checks if an argument of the given name exists (is set)
-     *
-     * @param string $argumentName Name of the argument to check
-     * @return bool TRUE if the argument is set, otherwise FALSE
+     * Checks if an argument of the given name exists (is set).
+     * @todo v12: public function hasArgument(string $argumentName): bool
      */
     public function hasArgument($argumentName);
 
     /**
-     * Returns an array of arguments and their values
+     * Return an instance with the specified argument set.
      *
-     * @return array Array of arguments and their values (which may be arguments and values as well)
+     * @param string $argumentName Name of the argument to set
+     * @param mixed $value The new value
+     * @return RequestInterface
+     * @todo v12: Enable
+     */
+    // public function withArgument(string $argumentName, $value): RequestInterface;
+
+    /**
+     * Returns an array of extbase arguments and their values.
+     * @todo v12: public function getArguments(): array
      */
     public function getArguments();
+
+    /**
+     * Return an instance with the specified extbase arguments, replacing
+     * any arguments which existed before.
+     * @todo v12: Enable
+     */
+    // public function withArguments(array $arguments): RequestInterface;
+
+    /**
+     * Returns the requested representation format, something
+     * like "html", "xml", "png", "json" or the like.
+     * @todo v12: Enable
+     */
+    // public function getFormat(): string;
+
+    /**
+     * Return an instance with the specified format.
+     *
+     * This method allows setting the format as described in getFormat().
+     * @todo v12: Enable
+     */
+    // public function withFormat(string $format): RequestInterface;
 }
diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/ReferringRequest.php b/typo3/sysext/extbase/Classes/Mvc/Web/ReferringRequest.php
index 19d78222d88e9b11c30487e11c53d6053929f3c6..1c98d52cda7f58574148708b67efa011932cb169 100644
--- a/typo3/sysext/extbase/Classes/Mvc/Web/ReferringRequest.php
+++ b/typo3/sysext/extbase/Classes/Mvc/Web/ReferringRequest.php
@@ -24,14 +24,10 @@ use TYPO3\CMS\Extbase\Mvc\Request;
  */
 class ReferringRequest extends Request
 {
-    /**
-     * @param string $controllerClassName
-     */
-    public function __construct(string $controllerClassName = '')
+    public function __construct($request = null)
     {
-        // @todo: Move to parent::__construct() in case Request is deprecated in v11, too, otherwise drop this todo.
         trigger_error(__CLASS__ . ' will be removed in TYPO3 v12, use ForwardResponse instead, see ActionController->forwardToReferringRequest().', E_USER_DEPRECATED);
-        parent::__construct($controllerClassName);
+        parent::__construct($request);
     }
 
     /**
@@ -46,16 +42,16 @@ class ReferringRequest extends Request
 
         switch ($argumentName) {
             case '@extension':
-                $this->setControllerExtensionName($value);
+                $this->getExtbaseAttribute()->setControllerExtensionName($value);
                 break;
             case '@controller':
-                $this->setControllerName($value);
+                $this->getExtbaseAttribute()->setControllerName($value);
                 break;
             case '@action':
-                $this->setControllerActionName($value);
+                $this->getExtbaseAttribute()->setControllerActionName($value);
                 break;
             case '@format':
-                $this->setFormat($value);
+                $this->getExtbaseAttribute()->setFormat($value);
                 break;
         }
     }
diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php b/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php
index a45316c4dafc9bdc971d7484b3a968c35ba611cc..f777ffa0afe839c17860b57966df40b143534c80 100644
--- a/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php
+++ b/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php
@@ -21,11 +21,11 @@ use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
 use TYPO3\CMS\Extbase\Mvc\Exception as MvcException;
 use TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException;
 use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException;
+use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
 use TYPO3\CMS\Extbase\Mvc\Request;
 use TYPO3\CMS\Extbase\Service\ExtensionService;
 
@@ -181,21 +181,22 @@ class RequestBuilder implements SingletonInterface
         $controllerClassName = $this->resolveControllerClassName($parameters);
         $actionName = $this->resolveActionName($controllerClassName, $parameters);
 
-        $request = GeneralUtility::makeInstance(Request::class);
-        $request->setPluginName($this->pluginName);
-        $request->setControllerExtensionName($this->extensionName);
-        $request->setControllerAliasToClassNameMapping($this->controllerAliasToClassMapping);
-        $request->setControllerName($this->controllerClassToAliasMapping[$controllerClassName]);
-        $request->setControllerActionName($actionName);
+        $extbaseAttribute = new ExtbaseRequestParameters();
+        $extbaseAttribute->setPluginName($this->pluginName);
+        $extbaseAttribute->setControllerExtensionName($this->extensionName);
+        $extbaseAttribute->setControllerAliasToClassNameMapping($this->controllerAliasToClassMapping);
+        $extbaseAttribute->setControllerName($this->controllerClassToAliasMapping[$controllerClassName]);
+        $extbaseAttribute->setControllerActionName($actionName);
+
         if (isset($parameters['format']) && is_string($parameters['format']) && $parameters['format'] !== '') {
-            $request->setFormat(filter_var($parameters['format'], FILTER_SANITIZE_STRING));
+            $extbaseAttribute->setFormat(filter_var($parameters['format'], FILTER_SANITIZE_STRING));
         } else {
-            $request->setFormat($this->defaultFormat);
+            $extbaseAttribute->setFormat($this->defaultFormat);
         }
         foreach ($parameters as $argumentName => $argumentValue) {
-            $request->setArgument($argumentName, $argumentValue);
+            $extbaseAttribute->setArgument($argumentName, $argumentValue);
         }
-        return $request;
+        return new Request($mainRequest->withAttribute('extbase', $extbaseAttribute));
     }
 
     /**
diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/RequestTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/RequestTest.php
index 4a57bbc49c904e94992fc228935e9f32d6971bf3..4ff550b96f181b8fd7a0de41cc66d881ef98390a 100644
--- a/typo3/sysext/extbase/Tests/Unit/Mvc/RequestTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Mvc/RequestTest.php
@@ -34,17 +34,6 @@ class RequestTest extends UnitTestCase
         self::assertEquals('theValue', $request->getArgument('someArgumentName'));
     }
 
-    /**
-     * @test
-     */
-    public function setArgumentThrowsExceptionIfTheGivenArgumentNameIsNoString(): void
-    {
-        $this->expectException(InvalidArgumentNameException::class);
-        $this->expectExceptionCode(1210858767);
-        $request = new Request();
-        $request->setArgument(123, 'theValue');
-    }
-
     /**
      * @test
      */
@@ -69,22 +58,6 @@ class RequestTest extends UnitTestCase
         self::assertEquals($arguments, $actualResult);
     }
 
-    /**
-     * @test
-     */
-    public function setArgumentsCallsSetArgumentForEveryArrayEntry(): void
-    {
-        $request = $this->getMockBuilder(Request::class)
-            ->onlyMethods(['setArgument'])
-            ->getMock();
-        $request->expects(self::exactly(2))->method('setArgument')
-            ->withConsecutive(
-                ['key1', 'value1'],
-                ['key2', 'value2']
-            );
-        $request->setArguments(['key1' => 'value1', 'key2' => 'value2']);
-    }
-
     /**
      * @test
      */
@@ -208,11 +181,7 @@ class RequestTest extends UnitTestCase
      */
     public function theActionNameCanBeSetAndRetrieved()
     {
-        $request = $this->getMockBuilder(Request::class)
-            ->onlyMethods(['getControllerObjectName'])
-            ->disableOriginalConstructor()
-            ->getMock();
-        $request->expects(self::once())->method('getControllerObjectName')->willReturn('');
+        $request = new Request();
         $request->setControllerActionName('theAction');
         self::assertEquals('theAction', $request->getControllerActionName());
     }