17
17
18
18
namespace TYPO3 \CMS \Core \Error \PageErrorHandler ;
19
19
20
+ use GuzzleHttp \Exception \ClientException ;
20
21
use Psr \Http \Message \ResponseFactoryInterface ;
21
22
use Psr \Http \Message \ResponseInterface ;
22
23
use Psr \Http \Message \ServerRequestInterface ;
23
24
use TYPO3 \CMS \Core \Cache \CacheManager ;
24
25
use TYPO3 \CMS \Core \Cache \Frontend \FrontendInterface ;
25
26
use TYPO3 \CMS \Core \Configuration \Features ;
27
+ use TYPO3 \CMS \Core \Controller \ErrorPageController ;
26
28
use TYPO3 \CMS \Core \Exception \SiteNotFoundException ;
27
29
use TYPO3 \CMS \Core \Http \HtmlResponse ;
28
30
use TYPO3 \CMS \Core \Http \RequestFactory ;
29
31
use TYPO3 \CMS \Core \Http \Response ;
30
32
use TYPO3 \CMS \Core \Http \Stream ;
31
33
use TYPO3 \CMS \Core \Http \Uri ;
32
34
use TYPO3 \CMS \Core \LinkHandling \LinkService ;
35
+ use TYPO3 \CMS \Core \Locking \Exception \LockAcquireWouldBlockException ;
36
+ use TYPO3 \CMS \Core \Locking \LockFactory ;
37
+ use TYPO3 \CMS \Core \Locking \LockingStrategyInterface ;
38
+ use TYPO3 \CMS \Core \Messaging \AbstractMessage ;
33
39
use TYPO3 \CMS \Core \Routing \InvalidRouteArgumentsException ;
34
40
use TYPO3 \CMS \Core \Site \Entity \Site ;
35
41
use TYPO3 \CMS \Core \Site \Entity \SiteLanguage ;
@@ -92,7 +98,7 @@ public function handlePageError(ServerRequestInterface $request, string $message
92
98
{
93
99
try {
94
100
$ urlParams = $ this ->link ->resolve ($ this ->errorHandlerConfiguration ['errorContentSource ' ]);
95
- $ urlParams ['pageuid ' ] = (int )($ urlParams ['pageuid ' ] ?? 0 );
101
+ $ this -> pageUid = $ urlParams ['pageuid ' ] = (int )($ urlParams ['pageuid ' ] ?? 0 );
96
102
$ resolvedUrl = $ this ->resolveUrl ($ request , $ urlParams );
97
103
98
104
// avoid denial-of-service amplification scenario
@@ -105,13 +111,24 @@ public function handlePageError(ServerRequestInterface $request, string $message
105
111
if ($ this ->useSubrequest ) {
106
112
// Create a subrequest and do not take any special query parameters into account
107
113
$ subRequest = $ request ->withQueryParams ([])->withUri (new Uri ($ resolvedUrl ))->withMethod ('GET ' );
108
- $ subResponse = $ this ->stashEnvironment (fn (): ResponseInterface => $ this ->sendSubRequest ($ subRequest , $ urlParams [ ' pageuid ' ] ));
114
+ $ subResponse = $ this ->stashEnvironment (fn (): ResponseInterface => $ this ->sendSubRequest ($ subRequest , $ this -> pageUid ));
109
115
} else {
116
+ $ cacheIdentifier = 'errorPage_ ' . md5 ($ resolvedUrl );
110
117
try {
111
- $ subResponse = $ this ->cachePageRequest ($ resolvedUrl , $ this ->pageUid , fn () => $ this ->sendRawRequest ($ resolvedUrl ));
118
+ $ subResponse = $ this ->cachePageRequest (
119
+ $ this ->pageUid ,
120
+ fn () => $ this ->sendRawRequest ($ resolvedUrl ),
121
+ $ cacheIdentifier
122
+ );
112
123
} catch (\Exception $ e ) {
113
124
throw new \RuntimeException (sprintf ('Error handler could not fetch error page "%s", reason: %s ' , $ resolvedUrl , $ e ->getMessage ()), 1544172838 , $ e );
114
125
}
126
+ // Ensure that 503 status code is kept, and not changed to 500.
127
+ if ($ subResponse ->getStatusCode () === 503 ) {
128
+ return $ this ->responseFactory ->createResponse ($ subResponse ->getStatusCode ())
129
+ ->withHeader ('content-type ' , $ subResponse ->getHeader ('content-type ' ))
130
+ ->withBody ($ subResponse ->getBody ());
131
+ }
115
132
}
116
133
117
134
if ($ subResponse ->getStatusCode () >= 300 ) {
@@ -144,40 +161,92 @@ protected function stashEnvironment(callable $fetcher): ResponseInterface
144
161
/**
145
162
* Caches a subrequest fetch.
146
163
*/
147
- protected function cachePageRequest (string $ resolvedUrl , int $ pageId , callable $ fetcher ): ResponseInterface
164
+ protected function cachePageRequest (int $ pageId , callable $ fetcher, string $ cacheIdentifier ): ResponseInterface
148
165
{
149
- $ cacheIdentifier = 'errorPage_ ' . md5 ($ resolvedUrl );
150
166
$ responseData = $ this ->cache ->get ($ cacheIdentifier );
151
-
152
- if (!is_array ($ responseData )) {
167
+ if (is_array ($ responseData ) && $ responseData !== []) {
168
+ return $ this ->createCachedPageRequestResponse ($ responseData );
169
+ }
170
+ $ cacheTags = [];
171
+ $ cacheTags [] = 'errorPage ' ;
172
+ if ($ pageId > 0 ) {
173
+ // Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes
174
+ $ cacheTags [] = 'pageId_ ' . $ pageId ;
175
+ }
176
+ $ lockFactory = GeneralUtility::makeInstance (LockFactory::class);
177
+ $ lock = $ lockFactory ->createLocker (
178
+ $ cacheIdentifier ,
179
+ LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
180
+ );
181
+ try {
182
+ $ locked = $ lock ->acquire (
183
+ LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
184
+ );
185
+ if (!$ locked ) {
186
+ return $ this ->createGenericErrorResponse ('Lock could not be acquired. ' );
187
+ }
153
188
/** @var ResponseInterface $response */
154
189
$ response = $ fetcher ();
155
- $ cacheTags = [];
156
- if ($ response ->getStatusCode () === 200 ) {
157
- $ cacheTags [] = 'errorPage ' ;
158
- if ($ pageId > 0 ) {
159
- // Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes
160
- $ cacheTags [] = 'pageId_ ' . $ pageId ;
161
- }
162
- $ responseData = [
163
- 'headers ' => $ response ->getHeaders (),
164
- 'body ' => $ response ->getBody ()->getContents (),
165
- 'reasonPhrase ' => $ response ->getReasonPhrase (),
166
- ];
167
- $ this ->cache ->set ($ cacheIdentifier , $ responseData , $ cacheTags );
190
+ if ($ response ->getStatusCode () !== 200 ) {
191
+ // External request lead to an error. Create a generic error response,
192
+ // cache and use that instead of the external error response.
193
+ $ response = $ this ->createGenericErrorResponse ('External error page could not be retrieved. ' );
168
194
}
169
- } else {
170
- $ body = new Stream ('php://temp ' , 'wb+ ' );
171
- $ body ->write ($ responseData ['body ' ] ?? '' );
172
- $ body ->rewind ();
173
- $ response = new Response (
174
- $ body ,
175
- 200 ,
176
- $ responseData ['headers ' ] ?? [],
177
- $ responseData ['reasonPhrase ' ] ?? ''
178
- );
195
+ $ responseData = [
196
+ 'statuscode ' => $ response ->getStatusCode (),
197
+ 'headers ' => $ response ->getHeaders (),
198
+ 'body ' => $ response ->getBody ()->getContents (),
199
+ 'reasonPhrase ' => $ response ->getReasonPhrase (),
200
+ ];
201
+ $ this ->cache ->set ($ cacheIdentifier , $ responseData , $ cacheTags );
202
+ $ lock ->release ();
203
+ } catch (ClientException $ e ) {
204
+ $ response = $ this ->createGenericErrorResponse ('External error page could not be retrieved. ' . $ e ->getMessage ());
205
+ $ responseData = [
206
+ 'statuscode ' => $ response ->getStatusCode (),
207
+ 'headers ' => $ response ->getHeaders (),
208
+ 'body ' => $ response ->getBody ()->getContents (),
209
+ 'reasonPhrase ' => $ response ->getReasonPhrase (),
210
+ ];
211
+ $ this ->cache ->set ($ cacheIdentifier , $ responseData , $ cacheTags );
212
+ } catch (LockAcquireWouldBlockException $ e ) {
213
+ // Currently a lock is active, thus returning a generic error directly to avoid
214
+ // long wait times and thus consuming too much php worker processes. Caching is
215
+ // not done here, as we do not know if the error page can be retrieved or not.
216
+ $ lock ->release ();
217
+ return $ this ->createGenericErrorResponse ('Lock could not be acquired. ' . $ e ->getMessage ());
218
+ } catch (\Throwable $ e ) {
219
+ // Any other error happened
220
+ $ lock ->release ();
221
+ return $ this ->createGenericErrorResponse ('Error page could not be retrieved ' . $ e ->getMessage ());
179
222
}
223
+ $ lock ->release ();
224
+ return $ this ->createCachedPageRequestResponse ($ responseData );
225
+ }
226
+
227
+ protected function createGenericErrorResponse (string $ message = '' ): ResponseInterface
228
+ {
229
+ $ content = GeneralUtility::makeInstance (ErrorPageController::class)->errorAction (
230
+ 'Page Not Found ' ,
231
+ $ message ?: 'Error page is being generated ' ,
232
+ AbstractMessage::ERROR ,
233
+ 0 ,
234
+ 503
235
+ );
236
+ return new HtmlResponse ($ content , 503 );
237
+ }
180
238
239
+ protected function createCachedPageRequestResponse (array $ responseData ): ResponseInterface
240
+ {
241
+ $ body = new Stream ('php://temp ' , 'wb+ ' );
242
+ $ body ->write ($ responseData ['body ' ] ?? '' );
243
+ $ body ->rewind ();
244
+ $ response = new Response (
245
+ $ body ,
246
+ $ responseData ['statuscode ' ] ?? 200 ,
247
+ $ responseData ['headers ' ] ?? [],
248
+ $ responseData ['reasonPhrase ' ] ?? ''
249
+ );
181
250
return $ response ;
182
251
}
183
252
@@ -215,7 +284,7 @@ protected function getSubRequestOptions(): array
215
284
$ options = [];
216
285
if ((int )$ GLOBALS ['TYPO3_CONF_VARS ' ]['HTTP ' ]['timeout ' ] === 0 ) {
217
286
$ options = [
218
- 'timeout ' => 30 ,
287
+ 'timeout ' => 10 ,
219
288
];
220
289
}
221
290
return $ options ;
0 commit comments