diff --git a/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php b/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php index b5c8e85ef54938af5eb406cb2409cb0231a30344..db82b31263f64b18a126a76345741e14682cddf5 100644 --- a/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php +++ b/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php @@ -17,12 +17,14 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Error\PageErrorHandler; +use GuzzleHttp\Exception\ClientException; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Configuration\Features; +use TYPO3\CMS\Core\Controller\ErrorPageController; use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Http\HtmlResponse; use TYPO3\CMS\Core\Http\RequestFactory; @@ -30,6 +32,10 @@ use TYPO3\CMS\Core\Http\Response; use TYPO3\CMS\Core\Http\Stream; use TYPO3\CMS\Core\Http\Uri; use TYPO3\CMS\Core\LinkHandling\LinkService; +use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException; +use TYPO3\CMS\Core\Locking\LockFactory; +use TYPO3\CMS\Core\Locking\LockingStrategyInterface; +use TYPO3\CMS\Core\Messaging\AbstractMessage; use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; @@ -92,7 +98,7 @@ class PageContentErrorHandler implements PageErrorHandlerInterface { try { $urlParams = $this->link->resolve($this->errorHandlerConfiguration['errorContentSource']); - $urlParams['pageuid'] = (int)($urlParams['pageuid'] ?? 0); + $this->pageUid = $urlParams['pageuid'] = (int)($urlParams['pageuid'] ?? 0); $resolvedUrl = $this->resolveUrl($request, $urlParams); // avoid denial-of-service amplification scenario @@ -105,13 +111,24 @@ class PageContentErrorHandler implements PageErrorHandlerInterface if ($this->useSubrequest) { // Create a subrequest and do not take any special query parameters into account $subRequest = $request->withQueryParams([])->withUri(new Uri($resolvedUrl))->withMethod('GET'); - $subResponse = $this->stashEnvironment(fn (): ResponseInterface => $this->sendSubRequest($subRequest, $urlParams['pageuid'])); + $subResponse = $this->stashEnvironment(fn (): ResponseInterface => $this->sendSubRequest($subRequest, $this->pageUid)); } else { + $cacheIdentifier = 'errorPage_' . md5($resolvedUrl); try { - $subResponse = $this->cachePageRequest($resolvedUrl, $this->pageUid, fn () => $this->sendRawRequest($resolvedUrl)); + $subResponse = $this->cachePageRequest( + $this->pageUid, + fn () => $this->sendRawRequest($resolvedUrl), + $cacheIdentifier + ); } catch (\Exception $e) { throw new \RuntimeException(sprintf('Error handler could not fetch error page "%s", reason: %s', $resolvedUrl, $e->getMessage()), 1544172838, $e); } + // Ensure that 503 status code is kept, and not changed to 500. + if ($subResponse->getStatusCode() === 503) { + return $this->responseFactory->createResponse($subResponse->getStatusCode()) + ->withHeader('content-type', $subResponse->getHeader('content-type')) + ->withBody($subResponse->getBody()); + } } if ($subResponse->getStatusCode() >= 300) { @@ -144,40 +161,92 @@ class PageContentErrorHandler implements PageErrorHandlerInterface /** * Caches a subrequest fetch. */ - protected function cachePageRequest(string $resolvedUrl, int $pageId, callable $fetcher): ResponseInterface + protected function cachePageRequest(int $pageId, callable $fetcher, string $cacheIdentifier): ResponseInterface { - $cacheIdentifier = 'errorPage_' . md5($resolvedUrl); $responseData = $this->cache->get($cacheIdentifier); - - if (!is_array($responseData)) { + if (is_array($responseData) && $responseData !== []) { + return $this->createCachedPageRequestResponse($responseData); + } + $cacheTags = []; + $cacheTags[] = 'errorPage'; + if ($pageId > 0) { + // Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes + $cacheTags[] = 'pageId_' . $pageId; + } + $lockFactory = GeneralUtility::makeInstance(LockFactory::class); + $lock = $lockFactory->createLocker( + $cacheIdentifier, + LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK + ); + try { + $locked = $lock->acquire( + LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK + ); + if (!$locked) { + return $this->createGenericErrorResponse('Lock could not be acquired.'); + } /** @var ResponseInterface $response */ $response = $fetcher(); - $cacheTags = []; - if ($response->getStatusCode() === 200) { - $cacheTags[] = 'errorPage'; - if ($pageId > 0) { - // Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes - $cacheTags[] = 'pageId_' . $pageId; - } - $responseData = [ - 'headers' => $response->getHeaders(), - 'body' => $response->getBody()->getContents(), - 'reasonPhrase' => $response->getReasonPhrase(), - ]; - $this->cache->set($cacheIdentifier, $responseData, $cacheTags); + if ($response->getStatusCode() !== 200) { + // External request lead to an error. Create a generic error response, + // cache and use that instead of the external error response. + $response = $this->createGenericErrorResponse('External error page could not be retrieved.'); } - } else { - $body = new Stream('php://temp', 'wb+'); - $body->write($responseData['body'] ?? ''); - $body->rewind(); - $response = new Response( - $body, - 200, - $responseData['headers'] ?? [], - $responseData['reasonPhrase'] ?? '' - ); + $responseData = [ + 'statuscode' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + 'body' => $response->getBody()->getContents(), + 'reasonPhrase' => $response->getReasonPhrase(), + ]; + $this->cache->set($cacheIdentifier, $responseData, $cacheTags); + $lock->release(); + } catch (ClientException $e) { + $response = $this->createGenericErrorResponse('External error page could not be retrieved. ' . $e->getMessage()); + $responseData = [ + 'statuscode' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + 'body' => $response->getBody()->getContents(), + 'reasonPhrase' => $response->getReasonPhrase(), + ]; + $this->cache->set($cacheIdentifier, $responseData, $cacheTags); + } catch (LockAcquireWouldBlockException $e) { + // Currently a lock is active, thus returning a generic error directly to avoid + // long wait times and thus consuming too much php worker processes. Caching is + // not done here, as we do not know if the error page can be retrieved or not. + $lock->release(); + return $this->createGenericErrorResponse('Lock could not be acquired. ' . $e->getMessage()); + } catch (\Throwable $e) { + // Any other error happened + $lock->release(); + return $this->createGenericErrorResponse('Error page could not be retrieved' . $e->getMessage()); } + $lock->release(); + return $this->createCachedPageRequestResponse($responseData); + } + + protected function createGenericErrorResponse(string $message = ''): ResponseInterface + { + $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction( + 'Page Not Found', + $message ?: 'Error page is being generated', + AbstractMessage::ERROR, + 0, + 503 + ); + return new HtmlResponse($content, 503); + } + protected function createCachedPageRequestResponse(array $responseData): ResponseInterface + { + $body = new Stream('php://temp', 'wb+'); + $body->write($responseData['body'] ?? ''); + $body->rewind(); + $response = new Response( + $body, + $responseData['statuscode'] ?? 200, + $responseData['headers'] ?? [], + $responseData['reasonPhrase'] ?? '' + ); return $response; } @@ -215,7 +284,7 @@ class PageContentErrorHandler implements PageErrorHandlerInterface $options = []; if ((int)$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout'] === 0) { $options = [ - 'timeout' => 30, + 'timeout' => 10, ]; } return $options;