Extbase controller action responses in TYPO3 v11+

In TYPO3 v11.0 the change was introduced that actions should return a PSR-7 response, it is deprecated not to do this. So if you want your extension to work in TYPO3 v10 and TYPO3 v11, leave it as it is for now.

However, if your extension only supports TYPO3 v11 and above, you should always return the PSR-7 response.

Behaviour before TYPO3 v11

Before TYPO3 v11, the developer usually did not return anything in an Extbase action:

public function listAction()
{
    $records = $this->recordRepository->findAll();
    $this->view->assign('records', $records);
}

However, you can also return the rendered content (usually a string) to be displayed on the web page, but this is optional:

public function listAction()
{
    $records = $this->recordRepository->findAll();
    $this->view->assign('records', $records);
    return $this->view->render();
}

Behaviour beginning with TYPO3 v11

With the release of TYPO3 v11.0 a \Psr\Http\Message\ResponseInterface should be returned.

Return HTML as response

The most common request is to return HTML content:

public function listAction(): \Psr\Http\Message\ResponseInterface
{
	$records = $this->recordRepository->findAll();
    $this->view->assign('records', $records);
	return $this->htmlResponse();
}

The htmlResponse() method ensures that the view's render() method is called. However, it can be called with the content to be displayed as an argument:

public function listAction(): Psr\Http\Message\ResponseInterface
{
    $records = $this->recordRepository->findAll();
    $this->view->assign('records', $records);
    return $this->htmlResponse($this->view->render());
}

Let's take a look into the htmlResponse() method of the \TYPO3\CMS\Extbase\Mvc\Controller\ActionController:

protected function htmlResponse(string $html = null): \Psr\Http\Message\ResponseInterface
{
    return $this->responseFactory->createResponse()
        ->withHeader('Content-Type', 'text/html; charset=utf-8')
        ->withBody($this->streamFactory->createStream($html ?? $this->view->render()));
}

A PSR-7 response factory implementation is used to create the response. The content type header is set to text/html and the body to the content (with the help of a PSR-7 stream factory implementation). So the htmlResponse() method is only a convenient way to return HTML content.

The responseFactory and the streamFactory properties are available in any Extbase controller which extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController. We'll come back to this later where this can be useful.

Return JSON as response

Since returning JSON is also a common use case, the handy jsonResponse() method is also available:

public function listAction(): \Psr\Http\Message\ResponseInterface
{
    // ... build JSON

    return $this->jsonResponse($json);
}

Like the htmlResponse(), the jsonResponse() accepts an optional parameter. If it is omitted, the view's render() method is called.

Forward a response to another action

To forward a response, create an instance of the \TYPO3\CMS\Extbase\Http\ForwardResponse class and add the necessary arguments via the with* methods. Note that a PSR response is always immutable, so you always get back a new instance of the class:

public function listAction(): \Psr\Http\Message\ResponseInterface
{
    return (new \TYPO3\CMS\Extbase\Http\ForwardResponse('list'))
        ->withExtensionName('AnotherExtension')
        ->withControllerName('AnotherController')
        ->withArguments(['parameter' => 'value']); 
}

Redirect to another URL

To create a redirect to another URL, you have to create the response object yourself. In the example, it is assumed that you want to redirect to another action, so the URI builder is used. We create the response using a PSR-17 response factory implementation, which is already available in controllers that extend \TYPO3\CMS\Extbase\Mvc\Controller\ActionController.

public function listAction(): \Psr\Http\Message\ResponseInterface
{
    // ... do something

    $uri = $this->uriBuilder->uriFor('show', ['parameter' => $value]);
    return $this->responseFactory->createResponse(307)
        ->withHeader('Location', $uri);
}

In this example, we create a temporary redirect (status code 307) and add the Location header with the URL as the value.

Provide a download

Downloading a file when calling an action is different from the previous examples because the returned response object is processed via the page renderer by default. After creating the response object, we need to throw a \TYPO3\CMS\Core\Http\PropagateResponseException to avoid this and return the response early. The exception code is the HTTP status code to use (in this case 200).

public function pdfAction(): \Psr\Http\Message\ResponseInterface
{
    // ... do something

    $response = $this->responseFactory->createResponse()
        ->withHeader('Cache-Control', 'private')
        ->withHeader('Content-Disposition', sprintf('attachment; filename="%s"', $filename)
        ->withHeader('Content-Length', (string)filesize($filePath))
        ->withHeader('Content-Type', 'application/pdf')
        ->withBody($this->streamFactory->createStreamFromFile($filePath));

    throw new PropagateResponseException($response, 200); 
}