Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7eb0d0b

Browse files
committedFeb 1, 2025
Refactor ExceptionHandlingMiddleware
1 parent bea6465 commit 7eb0d0b

9 files changed

+381
-488
lines changed
 

‎CHANGELOG.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- `ExceptionLoggingMiddleware` for custom error logging.
2323
- `ExceptionHandlingMiddleware` delegates exceptions to a custom error handler.
2424
- `ErrorHandlingMiddleware` converts errors into `ErrorException` instances that can then be handled by the `ExceptionHandlingMiddleware` and `ExceptionLoggingMiddleware`.
25-
- New custom error handlers using a new `ExceptionHandlerInterface`. See new `ExceptionHandlingMiddleware`.
26-
- New `JsonExceptionRenderer` generates a JSON problem details (rfc7807) response
27-
- New `XmlExceptionRenderer` generates a XML problem details (rfc7807) response
25+
- New custom error handlers using the new `ExceptionLoggingMiddleware` middleware.
26+
- New `JsonExceptionRenderer` generates JSON error response.
27+
- New `XmlExceptionRenderer` generates XML error response.
2828
- New `BasePathMiddleware` for dealing with Apache subdirectories.
2929
- New `HeadMethodMiddleware` ensures that the response body is empty for HEAD requests.
3030
- New `JsonRenderer` utility class for rendering JSON responses.
@@ -34,11 +34,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
- Support to build a custom middleware pipeline without the Slim App class. See new `ResponseFactoryMiddleware`
3535
- New media type detector
3636
- New Config class and ConfigInterface
37-
- New PhpDiContainerFactory class and ContainerFactoryInterface
37+
- New ContainerFactoryInterface and PhpDiContainerFactory class
3838

3939
### Changed
4040

41-
* Require PHP 8.2 or 8.3. News versions will be supported after a review and test process.
41+
* Require PHP 8.2 or newer. News versions will be supported after a review and test process.
4242
* Migrated all tests to PHPUnit 11
4343
* Update GitHub action and build settings
4444
* Improve DI container integration. Make the DI container a first-class citizen. Require a PSR-11 package.

‎Slim/Container/DefaultDefinitions.php

-49
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,12 @@
1919
use Psr\Log\NullLogger;
2020
use Slim\Configuration\Config;
2121
use Slim\Emitter\ResponseEmitter;
22-
use Slim\Error\Handlers\ExceptionHandler;
23-
use Slim\Error\Renderers\HtmlExceptionRenderer;
24-
use Slim\Error\Renderers\JsonExceptionRenderer;
25-
use Slim\Error\Renderers\PlainTextExceptionRenderer;
26-
use Slim\Error\Renderers\XmlExceptionRenderer;
2722
use Slim\Interfaces\ConfigurationInterface;
2823
use Slim\Interfaces\ContainerResolverInterface;
2924
use Slim\Interfaces\EmitterInterface;
30-
use Slim\Interfaces\ExceptionHandlerInterface;
3125
use Slim\Interfaces\RequestHandlerInvocationStrategyInterface;
32-
use Slim\Media\MediaType;
3326
use Slim\Media\MediaTypeDetector;
3427
use Slim\Middleware\BodyParsingMiddleware;
35-
use Slim\Middleware\ExceptionHandlingMiddleware;
36-
use Slim\Middleware\ExceptionLoggingMiddleware;
3728
use Slim\RequestHandler\MiddlewareRequestHandler;
3829
use Slim\Routing\Router;
3930
use Slim\Routing\Strategies\RequestResponse;
@@ -77,46 +68,6 @@ public function __invoke(): array
7768
return new ResponseEmitter();
7869
},
7970

80-
ExceptionHandlingMiddleware::class => function (ContainerInterface $container) {
81-
$handler = $container->get(ExceptionHandlerInterface::class);
82-
83-
return (new ExceptionHandlingMiddleware())->withExceptionHandler($handler);
84-
},
85-
86-
ExceptionHandlerInterface::class => function (ContainerInterface $container) {
87-
// Default exception handler
88-
$exceptionHandler = $container->get(ExceptionHandler::class);
89-
90-
// Settings
91-
$displayErrorDetails = (bool)$container->get(ConfigurationInterface::class)
92-
->get('display_error_details', false);
93-
94-
$exceptionHandler = $exceptionHandler
95-
->withDisplayErrorDetails($displayErrorDetails)
96-
->withDefaultMediaType(MediaType::TEXT_HTML);
97-
98-
return $exceptionHandler
99-
->withoutHandlers()
100-
->withHandler(MediaType::APPLICATION_JSON, JsonExceptionRenderer::class)
101-
->withHandler(MediaType::TEXT_HTML, HtmlExceptionRenderer::class)
102-
->withHandler(MediaType::APPLICATION_XHTML_XML, HtmlExceptionRenderer::class)
103-
->withHandler(MediaType::APPLICATION_XML, XmlExceptionRenderer::class)
104-
->withHandler(MediaType::TEXT_XML, XmlExceptionRenderer::class)
105-
->withHandler(MediaType::TEXT_PLAIN, PlainTextExceptionRenderer::class);
106-
},
107-
108-
ExceptionLoggingMiddleware::class => function (ContainerInterface $container) {
109-
// Default logger
110-
$logger = $container->get(LoggerInterface::class);
111-
$middleware = new ExceptionLoggingMiddleware($logger);
112-
113-
// Read settings
114-
$logErrorDetails = (bool)$container->get(ConfigurationInterface::class)
115-
->get('log_error_details', false);
116-
117-
return $middleware->withLogErrorDetails($logErrorDetails);
118-
},
119-
12071
LoggerInterface::class => function () {
12172
return new NullLogger();
12273
},

‎Slim/Error/Handlers/ExceptionHandler.php

-154
This file was deleted.

‎Slim/Interfaces/ExceptionHandlerInterface.php

-23
This file was deleted.

‎Slim/Middleware/CorsMiddleware.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public function withMaxAge(?int $maxAge): self
112112
* Set allowed origins. Null means allow all (*).
113113
* Pass an array of strings to specify allowed origins.
114114
*/
115-
public function withAllowedOrigins(?array $origins): self
115+
public function withAllowedOrigins(?array $origins = null): self
116116
{
117117
$clone = clone $this;
118118
$clone->allowedOrigins = $origins;

‎Slim/Middleware/ExceptionHandlingMiddleware.php

+141-8
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@
1010

1111
namespace Slim\Middleware;
1212

13+
use Psr\Container\ContainerInterface;
14+
use Psr\Http\Message\ResponseFactoryInterface;
1315
use Psr\Http\Message\ResponseInterface;
1416
use Psr\Http\Message\ServerRequestInterface;
1517
use Psr\Http\Server\MiddlewareInterface;
1618
use Psr\Http\Server\RequestHandlerInterface;
17-
use Slim\Interfaces\ExceptionHandlerInterface;
19+
use RuntimeException;
20+
use Slim\Exception\HttpException;
21+
use Slim\Exception\HttpMethodNotAllowedException;
22+
use Slim\Interfaces\ContainerResolverInterface;
23+
use Slim\Interfaces\ExceptionRendererInterface;
24+
use Slim\Media\MediaTypeDetector;
1825
use Throwable;
1926

2027
/**
@@ -27,26 +34,152 @@
2734
*/
2835
final class ExceptionHandlingMiddleware implements MiddlewareInterface
2936
{
30-
private ?ExceptionHandlerInterface $exceptionHandler = null;
37+
private ResponseFactoryInterface $responseFactory;
38+
39+
private MediaTypeDetector $mediaTypeDetector;
40+
41+
private ContainerResolverInterface $resolver;
42+
43+
private bool $displayErrorDetails = false;
44+
45+
private string $defaultMediaType = 'text/html';
46+
47+
private array $handlers = [];
48+
49+
/**
50+
* @var callable|ExceptionRendererInterface|string|null
51+
*/
52+
private $defaultHandler = null;
53+
54+
public function __construct(
55+
ContainerResolverInterface $resolver,
56+
ResponseFactoryInterface $responseFactory,
57+
MediaTypeDetector $mediaTypeDetector,
58+
) {
59+
$this->resolver = $resolver;
60+
$this->responseFactory = $responseFactory;
61+
$this->mediaTypeDetector = $mediaTypeDetector;
62+
}
63+
64+
public static function createFromContainer(ContainerInterface $container): self
65+
{
66+
return new self(
67+
$container->get(ContainerResolverInterface::class),
68+
$container->get(ResponseFactoryInterface::class),
69+
$container->get(MediaTypeDetector::class)
70+
);
71+
}
3172

3273
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
3374
{
3475
try {
3576
return $handler->handle($request);
3677
} catch (Throwable $exception) {
37-
if ($this->exceptionHandler) {
38-
return ($this->exceptionHandler)($request, $exception);
39-
}
78+
$statusCode = $this->determineStatusCode($request, $exception);
79+
$mediaType = $this->negotiateMediaType($request);
80+
$response = $this->createResponse($statusCode, $mediaType, $exception);
81+
$handler = $this->negotiateHandler($mediaType);
4082

41-
throw $exception;
83+
// Invoke the formatter handler
84+
return call_user_func(
85+
$handler,
86+
$request,
87+
$response,
88+
$exception,
89+
$this->displayErrorDetails
90+
);
4291
}
4392
}
4493

45-
public function withExceptionHandler(ExceptionHandlerInterface $exceptionHandler): self
94+
public function withDefaultMediaType(string $defaultMediaType): self
95+
{
96+
$clone = clone $this;
97+
$clone->defaultMediaType = $defaultMediaType;
98+
99+
return $clone;
100+
}
101+
102+
public function withDisplayErrorDetails(bool $displayErrorDetails): self
103+
{
104+
$clone = clone $this;
105+
$clone->displayErrorDetails = $displayErrorDetails;
106+
107+
return $clone;
108+
}
109+
110+
public function withDefaultHandler(ExceptionRendererInterface|callable|string $handler): self
111+
{
112+
$clone = clone $this;
113+
$clone->defaultHandler = $handler;
114+
115+
return $clone;
116+
}
117+
118+
public function withHandler(string $mediaType, ExceptionRendererInterface|callable|string $handler): self
46119
{
47120
$clone = clone $this;
48-
$clone->exceptionHandler = $exceptionHandler;
121+
$clone->handlers[$mediaType] = $handler;
49122

50123
return $clone;
51124
}
125+
126+
public function withoutHandlers(): self
127+
{
128+
$clone = clone $this;
129+
$clone->handlers = [];
130+
$clone->defaultHandler = null;
131+
132+
return $clone;
133+
}
134+
135+
private function negotiateMediaType(ServerRequestInterface $request): string
136+
{
137+
$mediaTypes = $this->mediaTypeDetector->detect($request);
138+
139+
return $mediaTypes[0] ?? $this->defaultMediaType;
140+
}
141+
142+
/**
143+
* Determine which handler to use based on media type.
144+
*/
145+
private function negotiateHandler(string $mediaType): callable
146+
{
147+
$handler = $this->handlers[$mediaType] ?? $this->defaultHandler ?? reset($this->handlers);
148+
149+
if (!$handler) {
150+
throw new RuntimeException(sprintf('Exception handler for "%s" not found', $mediaType));
151+
}
152+
153+
return $this->resolver->resolveCallable($handler);
154+
}
155+
156+
private function determineStatusCode(ServerRequestInterface $request, Throwable $exception): int
157+
{
158+
if ($exception instanceof HttpException) {
159+
return $exception->getCode();
160+
}
161+
162+
if ($request->getMethod() === 'OPTIONS') {
163+
return 200;
164+
}
165+
166+
return 500;
167+
}
168+
169+
private function createResponse(
170+
int $statusCode,
171+
string $contentType,
172+
Throwable $exception,
173+
): ResponseInterface {
174+
$response = $this->responseFactory
175+
->createResponse($statusCode)
176+
->withHeader('Content-Type', $contentType);
177+
178+
if ($exception instanceof HttpMethodNotAllowedException) {
179+
$allowedMethods = implode(', ', $exception->getAllowedMethods());
180+
$response = $response->withHeader('Allow', $allowedMethods);
181+
}
182+
183+
return $response;
184+
}
52185
}

‎tests/AppTest.php

+13-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use RuntimeException;
2222
use Slim\App;
2323
use Slim\Builder\AppBuilder;
24+
use Slim\Error\Renderers\HtmlExceptionRenderer;
2425
use Slim\Exception\HttpMethodNotAllowedException;
2526
use Slim\Exception\HttpNotFoundException;
2627
use Slim\Interfaces\ContainerFactoryInterface;
@@ -57,11 +58,21 @@ final class AppTest extends TestCase
5758
public function testAppWithExceptionAndErrorDetails(): void
5859
{
5960
$builder = new AppBuilder();
60-
$builder->addSettings(['display_error_details' => true]);
61+
$builder->addDefinitions(
62+
[
63+
ExceptionHandlingMiddleware::class => function ($container) {
64+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
65+
66+
return $middleware
67+
->withDisplayErrorDetails(true)
68+
->withDefaultHandler(HtmlExceptionRenderer::class);
69+
},
70+
]
71+
);
6172
$app = $builder->build();
6273

63-
$app->add(RoutingMiddleware::class);
6474
$app->add(ExceptionHandlingMiddleware::class);
75+
$app->add(RoutingMiddleware::class);
6576
$app->add(EndpointMiddleware::class);
6677

6778
$app->get('/', fn () => throw new UnexpectedValueException('Test exception message'));

‎tests/Error/Handlers/ExceptionHandlerTest.php

-162
This file was deleted.

‎tests/Middleware/ExceptionHandlingMiddlewareTest.php

+221-84
Original file line numberDiff line numberDiff line change
@@ -10,113 +10,75 @@
1010

1111
namespace Slim\Tests\Middleware;
1212

13+
use DOMDocument;
14+
use PHPUnit\Framework\Attributes\DataProvider;
1315
use PHPUnit\Framework\TestCase;
14-
use Psr\Http\Message\ResponseFactoryInterface;
15-
use Psr\Http\Message\ResponseInterface;
1616
use Psr\Http\Message\ServerRequestFactoryInterface;
17-
use Psr\Http\Message\ServerRequestInterface;
1817
use RuntimeException;
1918
use Slim\Builder\AppBuilder;
20-
use Slim\Interfaces\ExceptionHandlerInterface;
19+
use Slim\Error\Renderers\HtmlExceptionRenderer;
20+
use Slim\Error\Renderers\JsonExceptionRenderer;
21+
use Slim\Error\Renderers\XmlExceptionRenderer;
22+
use Slim\Media\MediaType;
2123
use Slim\Middleware\EndpointMiddleware;
2224
use Slim\Middleware\ExceptionHandlingMiddleware;
2325
use Slim\Middleware\RoutingMiddleware;
2426
use Slim\Tests\Traits\AppTestTrait;
25-
use Throwable;
2627

2728
final class ExceptionHandlingMiddlewareTest extends TestCase
2829
{
2930
use AppTestTrait;
3031

31-
public function testExceptionHandlingMiddlewareHandlesException()
32+
public function testDefaultHandlerWithoutDetails(): void
3233
{
3334
$builder = new AppBuilder();
35+
$builder->addDefinitions(
36+
[
37+
ExceptionHandlingMiddleware::class => function ($container) {
38+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
39+
40+
return $middleware
41+
->withDisplayErrorDetails(false)
42+
->withDefaultHandler(HtmlExceptionRenderer::class);
43+
},
44+
]
45+
);
3446
$app = $builder->build();
3547

36-
$responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class);
37-
38-
// Custom ExceptionHandlerInterface implementation
39-
$exceptionHandler = new class ($responseFactory) implements ExceptionHandlerInterface {
40-
private ResponseFactoryInterface $responseFactory;
41-
42-
public function __construct($responseFactory)
43-
{
44-
$this->responseFactory = $responseFactory;
45-
}
46-
47-
public function __invoke(ServerRequestInterface $request, Throwable $exception): ResponseInterface
48-
{
49-
$response = $this->responseFactory->createResponse(500, 'Internal Server Error');
50-
$response->getBody()->write($exception->getMessage());
51-
52-
return $response;
53-
}
54-
};
55-
56-
$app->add((new ExceptionHandlingMiddleware())->withExceptionHandler($exceptionHandler));
48+
$app->add(ExceptionHandlingMiddleware::class);
5749
$app->add(RoutingMiddleware::class);
5850
$app->add(EndpointMiddleware::class);
5951

60-
$app->get('/', function () {
61-
throw new RuntimeException('Something went wrong');
62-
});
63-
6452
$request = $app->getContainer()
6553
->get(ServerRequestFactoryInterface::class)
6654
->createServerRequest('GET', '/');
6755

68-
$response = $app->handle($request);
69-
$this->assertEquals(500, $response->getStatusCode());
70-
$this->assertSame('Something went wrong', (string)$response->getBody());
71-
}
72-
73-
public function testExceptionHandlingMiddlewarePassesThroughNonExceptionRequest()
74-
{
75-
$builder = new AppBuilder();
76-
$app = $builder->build();
77-
78-
$responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class);
79-
80-
// This handler should not be called in this test
81-
$exceptionHandler = new class ($responseFactory) implements ExceptionHandlerInterface {
82-
private ResponseFactoryInterface $responseFactory;
83-
84-
public function __construct($responseFactory)
85-
{
86-
$this->responseFactory = $responseFactory;
87-
}
88-
89-
public function __invoke(ServerRequestInterface $request, Throwable $exception): ResponseInterface
90-
{
91-
$response = $this->responseFactory->createResponse(500);
92-
$response->getBody()->write($exception->getMessage());
93-
94-
return $response;
95-
}
96-
};
97-
98-
$app->add(new ExceptionHandlingMiddleware($exceptionHandler));
99-
$app->add(RoutingMiddleware::class);
100-
$app->add(EndpointMiddleware::class);
101-
102-
$app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
103-
$response->getBody()->write('Hello World');
104-
105-
return $response;
56+
$app->get('/', function () {
57+
throw new RuntimeException('Test error');
10658
});
10759

108-
$request = $app->getContainer()
109-
->get(ServerRequestFactoryInterface::class)
110-
->createServerRequest('GET', '/');
111-
11260
$response = $app->handle($request);
113-
$this->assertEquals(200, $response->getStatusCode());
114-
$this->assertSame('Hello World', (string)$response->getBody());
61+
62+
$this->assertSame(500, $response->getStatusCode());
63+
$this->assertSame('text/html', $response->getHeaderLine('Content-Type'));
64+
$this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
65+
$this->assertStringContainsString('<h1>Application Error</h1>', (string)$response->getBody());
11566
}
11667

117-
public function testDefaultMediaTypeWithoutDetails(): void
68+
public function testDefaultHandlerWithDetails(): void
11869
{
11970
$builder = new AppBuilder();
71+
$builder->addDefinitions(
72+
[
73+
ExceptionHandlingMiddleware::class => function ($container) {
74+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
75+
76+
return $middleware
77+
->withDisplayErrorDetails(true)
78+
->withDefaultHandler(HtmlExceptionRenderer::class);
79+
},
80+
]
81+
);
12082
$app = $builder->build();
12183

12284
$app->add(ExceptionHandlingMiddleware::class);
@@ -128,21 +90,32 @@ public function testDefaultMediaTypeWithoutDetails(): void
12890
->createServerRequest('GET', '/');
12991

13092
$app->get('/', function () {
131-
throw new RuntimeException('Test error');
93+
throw new RuntimeException('Test error', 123);
13294
});
13395

13496
$response = $app->handle($request);
13597

13698
$this->assertSame(500, $response->getStatusCode());
137-
$this->assertSame('text/html', $response->getHeaderLine('Content-Type'));
99+
$this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
138100
$this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
139101
$this->assertStringContainsString('<h1>Application Error</h1>', (string)$response->getBody());
140102
}
141103

142104
public function testDefaultHtmlMediaTypeWithDetails(): void
143105
{
144106
$builder = new AppBuilder();
145-
$builder->addSettings(['display_error_details' => true]);
107+
$builder->addDefinitions(
108+
[
109+
ExceptionHandlingMiddleware::class => function ($container) {
110+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
111+
112+
return $middleware
113+
->withDisplayErrorDetails(true)
114+
->withDefaultMediaType(MediaType::TEXT_HTML)
115+
->withHandler(MediaType::TEXT_HTML, HtmlExceptionRenderer::class);
116+
},
117+
]
118+
);
146119
$app = $builder->build();
147120

148121
$app->add(ExceptionHandlingMiddleware::class);
@@ -151,7 +124,8 @@ public function testDefaultHtmlMediaTypeWithDetails(): void
151124

152125
$request = $app->getContainer()
153126
->get(ServerRequestFactoryInterface::class)
154-
->createServerRequest('GET', '/');
127+
->createServerRequest('GET', '/')
128+
->withHeader('Accept', 'application/json');
155129

156130
$app->get('/', function () {
157131
throw new RuntimeException('Test error', 123);
@@ -165,10 +139,22 @@ public function testDefaultHtmlMediaTypeWithDetails(): void
165139
$this->assertStringContainsString('<h1>Application Error</h1>', (string)$response->getBody());
166140
}
167141

168-
public function testJsonMediaTypeWithDetails(): void
142+
public function testJsonMediaTypeDisplayErrorDetails(): void
169143
{
170144
$builder = new AppBuilder();
171-
$builder->addSettings(['display_error_details' => true]);
145+
146+
$builder->addDefinitions(
147+
[
148+
ExceptionHandlingMiddleware::class => function ($container) {
149+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
150+
151+
return $middleware
152+
->withDisplayErrorDetails(true)
153+
->withHandler(MediaType::APPLICATION_JSON, JsonExceptionRenderer::class);
154+
},
155+
]
156+
);
157+
172158
$app = $builder->build();
173159

174160
$app->add(ExceptionHandlingMiddleware::class);
@@ -197,12 +183,12 @@ public function testJsonMediaTypeWithDetails(): void
197183
public function testWithoutHandler(): void
198184
{
199185
$this->expectException(RuntimeException::class);
200-
$this->expectExceptionMessage('Test error');
186+
$this->expectExceptionMessage('Exception handler for "text/html" not found');
201187

202188
$builder = new AppBuilder();
203189
$app = $builder->build();
204190

205-
$app->add(new ExceptionHandlingMiddleware());
191+
$app->add(ExceptionHandlingMiddleware::class);
206192
$app->add(RoutingMiddleware::class);
207193
$app->add(EndpointMiddleware::class);
208194

@@ -216,4 +202,155 @@ public function testWithoutHandler(): void
216202

217203
$app->handle($request);
218204
}
205+
206+
#[DataProvider('textHmlHeaderProvider')]
207+
public function testWithTextHtml(string $header, string $headerValue): void
208+
{
209+
$builder = new AppBuilder();
210+
$builder->addDefinitions(
211+
[
212+
ExceptionHandlingMiddleware::class => function ($container) {
213+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
214+
215+
return $middleware
216+
->withDisplayErrorDetails(true)
217+
->withHandler(MediaType::TEXT_HTML, HtmlExceptionRenderer::class);
218+
},
219+
]
220+
);
221+
$app = $builder->build();
222+
223+
$app->add(ExceptionHandlingMiddleware::class);
224+
$app->add(RoutingMiddleware::class);
225+
$app->add(EndpointMiddleware::class);
226+
227+
$app->get('/', function () {
228+
throw new RuntimeException('Test Error message');
229+
});
230+
231+
$request = $app->getContainer()
232+
->get(ServerRequestFactoryInterface::class)
233+
->createServerRequest('GET', '/')
234+
->withHeader($header, $headerValue);
235+
236+
$response = $app->handle($request);
237+
238+
$this->assertSame(500, $response->getStatusCode());
239+
$this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
240+
$this->assertStringContainsString('Test Error message', (string)$response->getBody());
241+
}
242+
243+
public static function textHmlHeaderProvider(): array
244+
{
245+
return [
246+
['Accept', 'text/html'],
247+
['Accept', 'text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8'],
248+
['Content-Type', 'text/html'],
249+
['Content-Type', 'text/html; charset=utf-8'],
250+
];
251+
}
252+
253+
// todo: Add test for other media types
254+
255+
public function testWithAcceptJson(): void
256+
{
257+
$builder = new AppBuilder();
258+
$builder->addDefinitions(
259+
[
260+
ExceptionHandlingMiddleware::class => function ($container) {
261+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
262+
263+
return $middleware
264+
->withDisplayErrorDetails(false)
265+
->withHandler(MediaType::APPLICATION_JSON, JsonExceptionRenderer::class);
266+
},
267+
]
268+
);
269+
$app = $builder->build();
270+
271+
$app->add(ExceptionHandlingMiddleware::class);
272+
$app->add(RoutingMiddleware::class);
273+
$app->add(EndpointMiddleware::class);
274+
275+
$request = $app->getContainer()
276+
->get(ServerRequestFactoryInterface::class)
277+
->createServerRequest('GET', '/')
278+
->withHeader('Accept', 'application/json');
279+
280+
$app->get('/', function () {
281+
throw new RuntimeException('Test exception');
282+
});
283+
284+
$response = $app->handle($request);
285+
286+
$this->assertSame(500, $response->getStatusCode());
287+
$expected = [
288+
'message' => 'Application Error',
289+
];
290+
$this->assertJsonResponse($expected, $response);
291+
}
292+
293+
public static function xmlHeaderProvider(): array
294+
{
295+
return [
296+
['Accept', 'application/xml'],
297+
['Accept', 'application/xml, application/json'],
298+
['Content-Type', 'application/xml'],
299+
['Content-Type', 'application/xml; charset=utf-8'],
300+
];
301+
}
302+
303+
#[DataProvider('xmlHeaderProvider')]
304+
public function testWithAcceptXml(string $header, string $headerValue): void
305+
{
306+
$builder = new AppBuilder();
307+
$builder->addDefinitions(
308+
[
309+
ExceptionHandlingMiddleware::class => function ($container) {
310+
$middleware = ExceptionHandlingMiddleware::createFromContainer($container);
311+
312+
return $middleware->withDisplayErrorDetails(false)
313+
->withoutHandlers()
314+
->withHandler('application/json', JsonExceptionRenderer::class)
315+
->withHandler('application/xml', XmlExceptionRenderer::class);
316+
},
317+
]
318+
);
319+
$app = $builder->build();
320+
321+
$app->add(ExceptionHandlingMiddleware::class);
322+
$app->add(RoutingMiddleware::class);
323+
$app->add(EndpointMiddleware::class);
324+
325+
$request = $app->getContainer()
326+
->get(ServerRequestFactoryInterface::class)
327+
->createServerRequest('GET', '/')
328+
->withHeader($header, $headerValue);
329+
330+
$app->get('/', function () {
331+
throw new RuntimeException('Test exception');
332+
});
333+
334+
$response = $app->handle($request);
335+
336+
$this->assertSame(500, $response->getStatusCode());
337+
$expected = '<?xml version="1.0" encoding="UTF-8"?>
338+
<error>
339+
<message>Application Error</message>
340+
</error>';
341+
342+
$dom = new DOMDocument();
343+
$dom->preserveWhiteSpace = false;
344+
$dom->formatOutput = true;
345+
$dom->loadXML($expected);
346+
$expected = $dom->saveXML();
347+
348+
$dom2 = new DOMDocument();
349+
$dom2->preserveWhiteSpace = false;
350+
$dom2->formatOutput = true;
351+
$dom2->loadXML((string)$response->getBody());
352+
$actual = $dom2->saveXML();
353+
354+
$this->assertSame($expected, $actual);
355+
}
219356
}

0 commit comments

Comments
 (0)
Please sign in to comment.