Skip to content

Commit 17a97f1

Browse files
authored
Merge pull request #3464 from iliaal/fix/error-handler-content-type-caching
Negotiate error handler content type on every request
2 parents 8e77282 + 4316531 commit 17a97f1

2 files changed

Lines changed: 100 additions & 2 deletions

File tree

Slim/Handlers/ErrorHandler.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ class ErrorHandler implements ErrorHandlerInterface
7878

7979
protected ?string $contentType = null;
8080

81+
protected bool $isContentTypeForced = false;
82+
8183
protected ?string $method = null;
8284

8385
protected ServerRequestInterface $request;
@@ -125,7 +127,7 @@ public function __invoke(
125127
$this->exception = $exception;
126128
$this->method = $request->getMethod();
127129
$this->statusCode = $this->determineStatusCode();
128-
if ($this->contentType === null) {
130+
if (!$this->isContentTypeForced) {
129131
$this->contentType = $this->determineContentType($request);
130132
}
131133

@@ -139,10 +141,11 @@ public function __invoke(
139141
/**
140142
* Force the content type for all error handler responses.
141143
*
142-
* @param string|null $contentType The content type
144+
* @param string|null $contentType The content type. Null restores Accept-header negotiation.
143145
*/
144146
public function forceContentType(?string $contentType): void
145147
{
148+
$this->isContentTypeForced = $contentType !== null;
146149
$this->contentType = $contentType;
147150
}
148151

tests/Handlers/ErrorHandlerTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,101 @@ public function testForceContentType()
118118
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
119119
}
120120

121+
/**
122+
* Test that the content type is negotiated from the Accept header on each
123+
* request instead of being cached from the first invocation.
124+
*/
125+
public function testContentTypeIsNegotiatedOnEachRequest()
126+
{
127+
$handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
128+
129+
$jsonRequest = $this
130+
->createServerRequest('/not-defined', 'GET')
131+
->withHeader('Accept', 'application/json');
132+
133+
$htmlRequest = $this
134+
->createServerRequest('/not-defined', 'GET')
135+
->withHeader('Accept', 'text/html');
136+
137+
$exception = new HttpNotFoundException($jsonRequest);
138+
139+
/** @var ResponseInterface $response */
140+
$response = $handler->__invoke($jsonRequest, $exception, false, false, false);
141+
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
142+
143+
$exception = new HttpNotFoundException($htmlRequest);
144+
145+
/** @var ResponseInterface $response */
146+
$response = $handler->__invoke($htmlRequest, $exception, false, false, false);
147+
$this->assertSame(['text/html'], $response->getHeader('Content-Type'));
148+
}
149+
150+
/**
151+
* Test that a forced content type still wins over per-request negotiation.
152+
*/
153+
public function testForcedContentTypeWinsOverSubsequentNegotiation()
154+
{
155+
$handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
156+
$handler->forceContentType('application/json');
157+
158+
$jsonRequest = $this
159+
->createServerRequest('/not-defined', 'GET')
160+
->withHeader('Accept', 'application/json');
161+
162+
$xmlRequest = $this
163+
->createServerRequest('/not-defined', 'GET')
164+
->withHeader('Accept', 'application/xml');
165+
166+
$exception = new HttpNotFoundException($jsonRequest);
167+
168+
/** @var ResponseInterface $response */
169+
$response = $handler->__invoke($jsonRequest, $exception, false, false, false);
170+
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
171+
172+
$exception = new HttpNotFoundException($xmlRequest);
173+
174+
/** @var ResponseInterface $response */
175+
$response = $handler->__invoke($xmlRequest, $exception, false, false, false);
176+
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
177+
}
178+
179+
/**
180+
* Test that forceContentType(null) restores Accept-header negotiation.
181+
*/
182+
public function testForceContentTypeNullRestoresNegotiation()
183+
{
184+
$handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
185+
$handler->forceContentType('application/json');
186+
187+
$xmlRequest = $this
188+
->createServerRequest('/not-defined', 'GET')
189+
->withHeader('Accept', 'application/xml');
190+
191+
$htmlRequest = $this
192+
->createServerRequest('/not-defined', 'GET')
193+
->withHeader('Accept', 'text/html');
194+
195+
$exception = new HttpNotFoundException($xmlRequest);
196+
197+
/** @var ResponseInterface $response */
198+
$response = $handler->__invoke($xmlRequest, $exception, false, false, false);
199+
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
200+
201+
$handler->forceContentType(null);
202+
203+
$exception = new HttpNotFoundException($xmlRequest);
204+
205+
/** @var ResponseInterface $response */
206+
$response = $handler->__invoke($xmlRequest, $exception, false, false, false);
207+
$this->assertSame(['application/xml'], $response->getHeader('Content-Type'));
208+
209+
$exception = new HttpNotFoundException($htmlRequest);
210+
211+
/** @var ResponseInterface $response */
212+
$response = $handler->__invoke($htmlRequest, $exception, false, false, false);
213+
$this->assertSame(['text/html'], $response->getHeader('Content-Type'));
214+
}
215+
121216
public function testHalfValidContentType()
122217
{
123218
$request = $this

0 commit comments

Comments
 (0)