diff --git a/CHANGELOG.md b/CHANGELOG.md
index 408858274..667fb8c68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,19 +18,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `RoutingMiddleware` handles the routing process.
- `EndpointMiddleware` processes the routing results and invokes the controller/action handler.
- Simplified Error handling concept. Relates to #3287.
- - Separation of Exceptions handling, PHP Error handling and Exception logging into different middleware.
+ - Separation of Exceptions handling, PHP Error handling and Exception logging into different middleware classes.
- `ExceptionLoggingMiddleware` for custom error logging.
- - `ExceptionHandlingMiddleware` delegates exceptions to a custom error handler.
- - `ErrorHandlingMiddleware` converts errors into `ErrorException` instances that can then be handled by the `ExceptionHandlingMiddleware` and `ExceptionLoggingMiddleware`.
- - New custom error handlers using the new `ExceptionLoggingMiddleware` middleware.
- - New `JsonExceptionRenderer` generates JSON error response.
- - New `XmlExceptionRenderer` generates XML error response.
-- New `BasePathMiddleware` for dealing with Apache subdirectories.
-- New `HeadMethodMiddleware` ensures that the response body is empty for HEAD requests.
-- New `JsonRenderer` utility class for rendering JSON responses.
-- New `RequestResponseTypedArgs` invocation strategy for route parameters with type declarations.
-- New `UrlGeneratorMiddleware` injects the `UrlGenerator` into the request attributes.
-- New `CorsMiddleware` for handling CORS requests.
+ - `JsonExceptionMiddleware` generates JSON error response.
+ - `HtmlExceptionMiddleware` generates HTML error response.
+ - `XmlExceptionMiddleware` generates XML error response.
+ - `PlainTextExceptionMiddleware` handles and formats exceptions into plain text responses.
+ - `ErrorExceptionMiddleware` converts PHP errors into `ErrorException` instances that can then be handled by the `ExceptionHandlingMiddleware` and `ExceptionLoggingMiddleware`.
+- `BasePathMiddleware` for dealing with Apache subdirectories.
+- `JsonBodyParserMiddleware` for parsing JSON requests.
+- `XmlBodyParserMiddleware` for parsing XML requests.
+- `FormUrlEncodedBodyParserMiddleware` for parsing form requests.
+- `CorsMiddleware` for handling CORS requests.
+- `UrlGeneratorMiddleware` injects the `UrlGenerator` into the request attributes.
+- `HeadMethodMiddleware` ensures that the response body is empty for HEAD requests.
+- `RequestResponseTypedArgs` invocation strategy for route parameters with type declarations.
- Support to build a custom middleware pipeline without the Slim App class. See new `ResponseFactoryMiddleware`
- New media type detector
- New ContainerFactoryInterface and PhpDiContainerFactory class
@@ -50,8 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
-* Remove LIFO middleware order support. Use FIFO instead.
+* LIFO middleware order support. Use FIFO instead.
* Router cache file support (File IO was never sufficient. PHP OpCache is much faster)
+* Removed `BodyParsingMiddlewareTest` in favor of `JsonBodyParserMiddleware`, `XmlBodyParserMiddleware` and `FormUrlEncodedBodyParserMiddleware`.
* The `$app->redirect()` method because it was not aware of the basePath. Use the `UrlGenerator` instead.
* The route `setArguments` and `setArgument` methods. Use a middleware for custom route arguments now.
* The `RouteContext::ROUTE` const. Use `$route = $request->getAttribute(RouteContext::ROUTING_RESULTS)->getRoute();` instead.
diff --git a/Slim/App.php b/Slim/App.php
index 6d6cc20c7..7d9f0ba3e 100644
--- a/Slim/App.php
+++ b/Slim/App.php
@@ -35,7 +35,7 @@
*
* @api
*/
-class App implements RouteCollectionInterface
+class App implements RequestHandlerInterface, RouteCollectionInterface
{
use RouteCollectionTrait;
diff --git a/Slim/Error/Renderers/ExceptionRendererTrait.php b/Slim/Error/Renderers/ExceptionRendererTrait.php
deleted file mode 100644
index 3eccba694..000000000
--- a/Slim/Error/Renderers/ExceptionRendererTrait.php
+++ /dev/null
@@ -1,39 +0,0 @@
-getTitle();
- }
-
- return $this->defaultErrorTitle;
- }
-
- private function getErrorDescription(Throwable $exception): string
- {
- if ($exception instanceof HttpException) {
- return $exception->getDescription();
- }
-
- return $this->defaultErrorDescription;
- }
-}
diff --git a/Slim/Error/Renderers/JsonExceptionRenderer.php b/Slim/Error/Renderers/JsonExceptionRenderer.php
deleted file mode 100644
index c06346f78..000000000
--- a/Slim/Error/Renderers/JsonExceptionRenderer.php
+++ /dev/null
@@ -1,66 +0,0 @@
-jsonRenderer = $jsonRenderer;
- }
-
- public function __invoke(
- ServerRequestInterface $request,
- ResponseInterface $response,
- ?Throwable $exception = null,
- bool $displayErrorDetails = false
- ): ResponseInterface {
- $error = ['message' => $this->getErrorTitle($exception)];
-
- if ($displayErrorDetails) {
- $error['exception'] = [];
- do {
- $error['exception'][] = $this->formatExceptionFragment($exception);
- } while ($exception = $exception->getPrevious());
- }
-
- return $this->jsonRenderer->json($response, $error);
- }
-
- private function formatExceptionFragment(Throwable $exception): array
- {
- $code = $exception instanceof ErrorException ? $exception->getSeverity() : $exception->getCode();
-
- return [
- 'type' => get_class($exception),
- 'code' => $code,
- 'message' => $exception->getMessage(),
- 'file' => $exception->getFile(),
- 'line' => $exception->getLine(),
- ];
- }
-}
diff --git a/Slim/Interfaces/ConfigurationInterface.php b/Slim/Interfaces/ConfigurationInterface.php
deleted file mode 100644
index edf040590..000000000
--- a/Slim/Interfaces/ConfigurationInterface.php
+++ /dev/null
@@ -1,16 +0,0 @@
-parseAcceptHeader($request->getHeaderLine('Accept'));
-
- if (!$mediaTypes) {
- $mediaTypes = $this->parseContentType($request->getHeaderLine('Content-Type'));
- }
-
- return $mediaTypes;
- }
-
- /**
- * Parses the 'Accept' header to extract media types.
- *
- * This method splits the 'Accept' header value into its components and normalizes
- * the media types by trimming whitespace and converting them to lowercase.
- *
- * This method doesn't consider the quality values (q-values) that can be present in the Accept header.
- * If prioritization is important for your use case, you might want to consider implementing
- * q-value parsing and sorting.
- *
- * @param string|null $accept the value of the 'Accept' header
- *
- * @return array an array of normalized media types from the 'Accept' header
- */
- private function parseAcceptHeader(?string $accept): array
- {
- $acceptTypes = $accept ? explode(',', $accept) : [];
-
- // Normalize types
- $cleanTypes = [];
- foreach ($acceptTypes as $type) {
- $tokens = explode(';', $type);
- $name = trim(strtolower(reset($tokens)));
- $cleanTypes[] = $name;
- }
-
- return $cleanTypes;
- }
-
- /**
- * Parses the 'Content-Type' header to extract the media type.
- *
- * This method splits the 'Content-Type' header value to separate the media type
- * from any additional parameters, normalizes it, and returns it in an array.
- *
- * @param string|null $contentType the value of the 'Content-Type' header
- *
- * @return array an array containing the normalized media type from the 'Content-Type' header
- */
- private function parseContentType(?string $contentType): array
- {
- if ($contentType === null) {
- return [];
- }
-
- $parts = explode(';', $contentType);
- $name = strtolower(trim($parts[0]));
-
- return $name ? [$name] : [];
- }
-}
diff --git a/Slim/Middleware/BodyParsingMiddleware.php b/Slim/Middleware/BodyParsingMiddleware.php
deleted file mode 100644
index 381825135..000000000
--- a/Slim/Middleware/BodyParsingMiddleware.php
+++ /dev/null
@@ -1,140 +0,0 @@
-mediaTypeDetector = $mediaTypeDetector;
- }
-
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
- {
- $parsedBody = $request->getParsedBody();
-
- if (empty($parsedBody)) {
- $parsedBody = $this->parseBody($request);
- $request = $request->withParsedBody($parsedBody);
- }
-
- return $handler->handle($request);
- }
-
- /**
- * @param string $mediaType The HTTP media type (excluding content-type params)
- * @param callable $handler The callable that returns parsed contents for media type
- */
- public function withBodyParser(string $mediaType, callable $handler): self
- {
- $clone = clone $this;
- $clone->handlers[$mediaType] = $handler;
-
- return $clone;
- }
-
- public function withDefaultMediaType(string $mediaType): self
- {
- $clone = clone $this;
- $clone->defaultMediaType = $mediaType;
-
- return $clone;
- }
-
- public function withDefaultBodyParsers(): self
- {
- $clone = clone $this;
- $clone = $clone->withBodyParser(MediaType::APPLICATION_JSON, function ($input) {
- $result = json_decode($input, true);
-
- if (!is_array($result)) {
- return null;
- }
-
- return $result;
- });
-
- $clone = $clone->withBodyParser(MediaType::APPLICATION_FORM_URLENCODED, function ($input) {
- parse_str($input, $data);
-
- return $data;
- });
-
- $xmlCallable = function ($input) {
- $backup_errors = libxml_use_internal_errors(true);
- $result = simplexml_load_string($input);
-
- libxml_clear_errors();
- libxml_use_internal_errors($backup_errors);
-
- if ($result === false) {
- return null;
- }
-
- return $result;
- };
-
- return $clone
- ->withBodyParser(MediaType::APPLICATION_XML, $xmlCallable)
- ->withBodyParser(MediaType::TEXT_XML, $xmlCallable);
- }
-
- /**
- * Parse request body.
- *
- * @throws RuntimeException
- */
- private function parseBody(ServerRequestInterface $request): array|object|null
- {
- // Negotiate content type
- $contentTypes = $this->mediaTypeDetector->detect($request);
- $contentType = $contentTypes[0] ?? $this->defaultMediaType;
-
- // Determine which handler to use based on media type
- $handler = $this->handlers[$contentType] ?? reset($this->handlers);
-
- // Invoke the parser
- $parsed = call_user_func(
- $handler,
- (string)$request->getBody()
- );
-
- if ($parsed === null || is_object($parsed) || is_array($parsed)) {
- return $parsed;
- }
-
- throw new RuntimeException(
- 'Request body media type parser return value must be an array, an object, or null.'
- );
- }
-}
diff --git a/Slim/Middleware/ErrorHandlingMiddleware.php b/Slim/Middleware/ErrorExceptionMiddleware.php
similarity index 93%
rename from Slim/Middleware/ErrorHandlingMiddleware.php
rename to Slim/Middleware/ErrorExceptionMiddleware.php
index cdeb6c650..9fa81e6af 100644
--- a/Slim/Middleware/ErrorHandlingMiddleware.php
+++ b/Slim/Middleware/ErrorExceptionMiddleware.php
@@ -13,7 +13,7 @@
/**
* Converts errors into ErrorException instances.
*/
-final class ErrorHandlingMiddleware implements MiddlewareInterface
+final class ErrorExceptionMiddleware implements MiddlewareInterface
{
/**
* @throws ErrorException
diff --git a/Slim/Middleware/ExceptionHandlingMiddleware.php b/Slim/Middleware/ExceptionHandlingMiddleware.php
deleted file mode 100644
index e55f32b8f..000000000
--- a/Slim/Middleware/ExceptionHandlingMiddleware.php
+++ /dev/null
@@ -1,185 +0,0 @@
-resolver = $resolver;
- $this->responseFactory = $responseFactory;
- $this->mediaTypeDetector = $mediaTypeDetector;
- }
-
- public static function createFromContainer(ContainerInterface $container): self
- {
- return new self(
- $container->get(ContainerResolverInterface::class),
- $container->get(ResponseFactoryInterface::class),
- $container->get(MediaTypeDetector::class)
- );
- }
-
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
- {
- try {
- return $handler->handle($request);
- } catch (Throwable $exception) {
- $statusCode = $this->determineStatusCode($request, $exception);
- $mediaType = $this->negotiateMediaType($request);
- $response = $this->createResponse($statusCode, $mediaType, $exception);
- $handler = $this->negotiateHandler($mediaType);
-
- // Invoke the formatter handler
- return call_user_func(
- $handler,
- $request,
- $response,
- $exception,
- $this->displayErrorDetails
- );
- }
- }
-
- public function withDefaultMediaType(string $defaultMediaType): self
- {
- $clone = clone $this;
- $clone->defaultMediaType = $defaultMediaType;
-
- return $clone;
- }
-
- public function withDisplayErrorDetails(bool $displayErrorDetails): self
- {
- $clone = clone $this;
- $clone->displayErrorDetails = $displayErrorDetails;
-
- return $clone;
- }
-
- public function withDefaultHandler(ExceptionRendererInterface|callable|string $handler): self
- {
- $clone = clone $this;
- $clone->defaultHandler = $handler;
-
- return $clone;
- }
-
- public function withHandler(string $mediaType, ExceptionRendererInterface|callable|string $handler): self
- {
- $clone = clone $this;
- $clone->handlers[$mediaType] = $handler;
-
- return $clone;
- }
-
- public function withoutHandlers(): self
- {
- $clone = clone $this;
- $clone->handlers = [];
- $clone->defaultHandler = null;
-
- return $clone;
- }
-
- private function negotiateMediaType(ServerRequestInterface $request): string
- {
- $mediaTypes = $this->mediaTypeDetector->detect($request);
-
- return $mediaTypes[0] ?? $this->defaultMediaType;
- }
-
- /**
- * Determine which handler to use based on media type.
- */
- private function negotiateHandler(string $mediaType): callable
- {
- $handler = $this->handlers[$mediaType] ?? $this->defaultHandler ?? reset($this->handlers);
-
- if (!$handler) {
- throw new RuntimeException(sprintf('Exception handler for "%s" not found', $mediaType));
- }
-
- return $this->resolver->resolveCallable($handler);
- }
-
- private function determineStatusCode(ServerRequestInterface $request, Throwable $exception): int
- {
- if ($exception instanceof HttpException) {
- return $exception->getCode();
- }
-
- if ($request->getMethod() === 'OPTIONS') {
- return 200;
- }
-
- return 500;
- }
-
- private function createResponse(
- int $statusCode,
- string $contentType,
- Throwable $exception,
- ): ResponseInterface {
- $response = $this->responseFactory
- ->createResponse($statusCode)
- ->withHeader('Content-Type', $contentType);
-
- if ($exception instanceof HttpMethodNotAllowedException) {
- $allowedMethods = implode(', ', $exception->getAllowedMethods());
- $response = $response->withHeader('Allow', $allowedMethods);
- }
-
- return $response;
- }
-}
diff --git a/Slim/Middleware/ExceptionMiddlewareTrait.php b/Slim/Middleware/ExceptionMiddlewareTrait.php
new file mode 100644
index 000000000..54346dd42
--- /dev/null
+++ b/Slim/Middleware/ExceptionMiddlewareTrait.php
@@ -0,0 +1,130 @@
+ 1];
+
+ public function __construct(ResponseFactoryInterface $responseFactory)
+ {
+ $this->responseFactory = $responseFactory;
+ }
+
+ public function withErrorDetails(bool $flag): self
+ {
+ $clone = clone $this;
+ $clone->displayErrorDetails = $flag;
+
+ return $clone;
+ }
+
+ public function withMimeType(string $mimeType): self
+ {
+ $clone = clone $this;
+ $clone->mimeTypes[$mimeType] = 1;
+
+ return $clone;
+ }
+
+ private function getErrorTitle(Throwable $exception): string
+ {
+ if ($exception instanceof HttpException) {
+ return $exception->getTitle();
+ }
+
+ return $this->defaultErrorTitle;
+ }
+
+ private function getErrorDescription(Throwable $exception): string
+ {
+ if ($exception instanceof HttpException) {
+ return $exception->getDescription();
+ }
+
+ return $this->defaultErrorDescription;
+ }
+
+ private function detectMediaType(ServerRequestInterface $request): ?string
+ {
+ $accept = $request->getHeaderLine('Accept');
+
+ // Performance optimized for most cases
+ if ($accept === self::DEFAULT_TYPE || isset($this->mimeTypes['*/*'])) {
+ return self::DEFAULT_TYPE;
+ }
+
+ // Parses complex Accept headers
+ $mimeTypes = $this->parseAcceptHeader($accept);
+
+ foreach ($mimeTypes as $type) {
+ if (isset($this->mimeTypes[$type])) {
+ return $type;
+ }
+ }
+
+ return null;
+ }
+
+ private function createResponse(Throwable $exception, string $payload, string $contentType): ResponseInterface
+ {
+ $response = $this->responseFactory
+ ->createResponse(500)
+ ->withHeader('Content-Type', $contentType);
+
+ $response->getBody()->write($payload);
+
+ if ($exception instanceof HttpMethodNotAllowedException) {
+ $allowedMethods = implode(', ', $exception->getAllowedMethods());
+ $response = $response->withHeader('Allow', $allowedMethods);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Parses the 'Accept' header to extract media types.
+ * This method doesn't consider the quality values (q-values).
+ *
+ * @param string|null $accept The value of the 'Accept' header
+ *
+ * @return array An array of normalized media types from the 'Accept' header
+ */
+ public function parseAcceptHeader(?string $accept): array
+ {
+ $acceptTypes = $accept ? explode(',', $accept) : [];
+
+ $cleanTypes = [];
+
+ foreach ($acceptTypes as $type) {
+ // Remove optional parameters like ";q=0.8"
+ $name = strtolower(trim(explode(';', $type)[0]));
+ $cleanTypes[] = $name;
+ }
+
+ return $cleanTypes;
+ }
+}
diff --git a/Slim/Middleware/FormUrlEncodedBodyParserMiddleware.php b/Slim/Middleware/FormUrlEncodedBodyParserMiddleware.php
new file mode 100644
index 000000000..af6b6d1b7
--- /dev/null
+++ b/Slim/Middleware/FormUrlEncodedBodyParserMiddleware.php
@@ -0,0 +1,38 @@
+getMethod();
+ $contentType = $request->getHeaderLine('Content-Type');
+
+ if (!in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
+ return $handler->handle($request);
+ }
+
+ if ($this->isFormUrlEncodedMediaType($contentType)) {
+ $body = (string)$request->getBody();
+ parse_str($body, $parsed);
+ $request = $request->withParsedBody($parsed);
+ }
+
+ return $handler->handle($request);
+ }
+
+ private function isFormUrlEncodedMediaType(string $contentType): bool
+ {
+ $type = strtolower(trim(explode(';', $contentType)[0]));
+
+ return $type === 'application/x-www-form-urlencoded';
+ }
+}
diff --git a/Slim/Middleware/HeadMethodMiddleware.php b/Slim/Middleware/HeadMethodMiddleware.php
index faf15b11e..1bc9e9bb1 100644
--- a/Slim/Middleware/HeadMethodMiddleware.php
+++ b/Slim/Middleware/HeadMethodMiddleware.php
@@ -10,9 +10,9 @@
namespace Slim\Middleware;
-use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -25,11 +25,11 @@
*/
final class HeadMethodMiddleware implements MiddlewareInterface
{
- private ResponseFactoryInterface $responseFactory;
+ private StreamFactoryInterface $streamFactory;
- public function __construct(ResponseFactoryInterface $responseFactory)
+ public function __construct(StreamFactoryInterface $streamFactory)
{
- $this->responseFactory = $responseFactory;
+ $this->streamFactory = $streamFactory;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
@@ -45,9 +45,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
*/
$method = strtoupper($request->getMethod());
if ($method === 'HEAD') {
- $emptyBody = $this->responseFactory->createResponse()->getBody();
-
- return $response->withBody($emptyBody);
+ return $response->withBody($this->streamFactory->createStream());
}
return $response;
diff --git a/Slim/Error/Renderers/HtmlExceptionRenderer.php b/Slim/Middleware/HtmlExceptionMiddleware.php
similarity index 72%
rename from Slim/Error/Renderers/HtmlExceptionRenderer.php
rename to Slim/Middleware/HtmlExceptionMiddleware.php
index d9ca168ce..78becbce2 100644
--- a/Slim/Error/Renderers/HtmlExceptionRenderer.php
+++ b/Slim/Middleware/HtmlExceptionMiddleware.php
@@ -8,40 +8,39 @@
declare(strict_types=1);
-namespace Slim\Error\Renderers;
+namespace Slim\Middleware;
use ErrorException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamFactoryInterface;
-use Slim\Interfaces\ExceptionRendererInterface;
-use Slim\Media\MediaType;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
-use function get_class;
-use function sprintf;
-
-/**
- * Formats exceptions into a HTML response.
- */
-final class HtmlExceptionRenderer implements ExceptionRendererInterface
+final class HtmlExceptionMiddleware implements MiddlewareInterface
{
- use ExceptionRendererTrait;
+ use ExceptionMiddlewareTrait;
- private StreamFactoryInterface $streamFactory;
+ private const DEFAULT_TYPE = 'text/html';
- public function __construct(StreamFactoryInterface $streamFactory)
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
- $this->streamFactory = $streamFactory;
+ try {
+ return $handler->handle($request);
+ } catch (Throwable $exception) {
+ $contentType = $this->detectMediaType($request);
+
+ if ($contentType === null) {
+ throw $exception;
+ }
+
+ return $this->createResponse($exception, $this->createPayload($exception), $contentType);
+ }
}
- public function __invoke(
- ServerRequestInterface $request,
- ResponseInterface $response,
- ?Throwable $exception = null,
- bool $displayErrorDetails = false
- ): ResponseInterface {
- if ($displayErrorDetails) {
+ private function createPayload(Throwable $exception): string
+ {
+ if ($this->displayErrorDetails) {
$html = '
The application could not run because of the following error:
';
$html .= 'Details
';
$html .= $this->renderExceptionFragment($exception);
@@ -49,12 +48,7 @@ public function __invoke(
$html = sprintf('%s
', $this->getErrorDescription($exception));
}
- $html = $this->renderHtmlBody($this->getErrorTitle($exception), $html);
-
- $body = $this->streamFactory->createStream($html);
- $response = $response->withBody($body);
-
- return $response->withHeader('Content-Type', MediaType::TEXT_HTML);
+ return $this->renderHtmlBody($this->getErrorTitle($exception), $html);
}
private function renderExceptionFragment(Throwable $exception): string
@@ -88,7 +82,7 @@ private function renderExceptionFragment(Throwable $exception): string
return $html;
}
- public function renderHtmlBody(string $title = '', string $html = ''): string
+ private function renderHtmlBody(string $title = '', string $html = ''): string
{
return sprintf(
'' .
diff --git a/Slim/Middleware/JsonBodyParserMiddleware.php b/Slim/Middleware/JsonBodyParserMiddleware.php
new file mode 100644
index 000000000..6eef03fd0
--- /dev/null
+++ b/Slim/Middleware/JsonBodyParserMiddleware.php
@@ -0,0 +1,62 @@
+flags = $jsonFlags;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $method = $request->getMethod();
+ $contentType = $request->getHeaderLine('Content-Type');
+
+ if (!in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
+ return $handler->handle($request);
+ }
+
+ if ($this->isJsonMediaType($contentType)) {
+ $body = (string)$request->getBody();
+ $parsed = json_decode($body, true, 512, $this->flags);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new HttpBadRequestException($request, sprintf('Invalid JSON body: %s', json_last_error_msg()));
+ }
+
+ if (is_array($parsed)) {
+ $request = $request->withParsedBody($parsed);
+ }
+ }
+
+ return $handler->handle($request);
+ }
+
+ /**
+ * Check whether the content type is JSON or has a +json structured suffix.
+ */
+ private function isJsonMediaType(string $contentType): bool
+ {
+ $contentType = strtolower(trim(explode(';', $contentType)[0]));
+
+ return $contentType === 'application/json' || str_ends_with($contentType, '+json');
+ }
+}
diff --git a/Slim/Middleware/JsonExceptionMiddleware.php b/Slim/Middleware/JsonExceptionMiddleware.php
new file mode 100644
index 000000000..5306059e9
--- /dev/null
+++ b/Slim/Middleware/JsonExceptionMiddleware.php
@@ -0,0 +1,83 @@
+handle($request);
+ } catch (Throwable $exception) {
+ $contentType = $this->detectMediaType($request);
+
+ if ($contentType === null) {
+ throw $exception;
+ }
+
+ return $this->createResponse($exception, $this->createPayload($exception), $contentType);
+ }
+ }
+
+ /**
+ * Set options for JSON encoding.
+ *
+ * @see https://php.net/manual/function.json-encode.php
+ * @see https://php.net/manual/json.constants.php
+ */
+ public function withJsonOptions(int $options): self
+ {
+ $clone = clone $this;
+ $clone->jsonOptions = $options;
+
+ return $clone;
+ }
+
+ private function createPayload(Throwable $exception): string
+ {
+ $payload = ['message' => $this->getErrorTitle($exception)];
+
+ if ($this->displayErrorDetails) {
+ $payload['exception'] = [];
+ do {
+ $payload['exception'][] = $this->formatExceptionFragment($exception);
+ } while ($exception = $exception->getPrevious());
+ }
+
+ return (string)json_encode($payload, $this->jsonOptions);
+ }
+
+ private function formatExceptionFragment(Throwable $exception): array
+ {
+ $code = $exception instanceof ErrorException ? $exception->getSeverity() : $exception->getCode();
+
+ return [
+ 'type' => get_class($exception),
+ 'code' => $code,
+ 'message' => $exception->getMessage(),
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ ];
+ }
+}
diff --git a/Slim/Error/Renderers/PlainTextExceptionRenderer.php b/Slim/Middleware/PlainTextExceptionMiddleware.php
similarity index 56%
rename from Slim/Error/Renderers/PlainTextExceptionRenderer.php
rename to Slim/Middleware/PlainTextExceptionMiddleware.php
index 2cc625fe1..5a84cd7aa 100644
--- a/Slim/Error/Renderers/PlainTextExceptionRenderer.php
+++ b/Slim/Middleware/PlainTextExceptionMiddleware.php
@@ -8,42 +8,41 @@
declare(strict_types=1);
-namespace Slim\Error\Renderers;
+namespace Slim\Middleware;
use ErrorException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamFactoryInterface;
-use Slim\Interfaces\ExceptionRendererInterface;
-use Slim\Media\MediaType;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
-use function get_class;
-use function sprintf;
-
-/**
- * Formats exceptions into a plain text response.
- */
-final class PlainTextExceptionRenderer implements ExceptionRendererInterface
+final class PlainTextExceptionMiddleware implements MiddlewareInterface
{
- use ExceptionRendererTrait;
+ use ExceptionMiddlewareTrait;
- private StreamFactoryInterface $streamFactory;
+ private const DEFAULT_TYPE = 'text/plain';
- public function __construct(StreamFactoryInterface $streamFactory)
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
- $this->streamFactory = $streamFactory;
+ try {
+ return $handler->handle($request);
+ } catch (Throwable $exception) {
+ $contentType = $this->detectMediaType($request);
+
+ if ($contentType === null) {
+ throw $exception;
+ }
+
+ return $this->createResponse($exception, $this->createPayload($exception), $contentType);
+ }
}
- public function __invoke(
- ServerRequestInterface $request,
- ResponseInterface $response,
- ?Throwable $exception = null,
- bool $displayErrorDetails = false
- ): ResponseInterface {
+ private function createPayload(Throwable $exception): string
+ {
$text = sprintf("%s\n", $this->getErrorTitle($exception));
- if ($displayErrorDetails) {
+ if ($this->displayErrorDetails) {
$text .= $this->formatExceptionFragment($exception);
while ($exception = $exception->getPrevious()) {
@@ -52,10 +51,7 @@ public function __invoke(
}
}
- $body = $this->streamFactory->createStream($text);
- $response = $response->withBody($body);
-
- return $response->withHeader('Content-Type', MediaType::TEXT_PLAIN);
+ return $text;
}
private function formatExceptionFragment(Throwable $exception): string
diff --git a/Slim/Middleware/XmlBodyParserMiddleware.php b/Slim/Middleware/XmlBodyParserMiddleware.php
new file mode 100644
index 000000000..1933ee8a5
--- /dev/null
+++ b/Slim/Middleware/XmlBodyParserMiddleware.php
@@ -0,0 +1,56 @@
+getMethod();
+ $contentType = $request->getHeaderLine('Content-Type');
+
+ if (!in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
+ return $handler->handle($request);
+ }
+
+ if ($this->isXmlMediaType($contentType)) {
+ $backup = libxml_use_internal_errors(true);
+ $body = (string)$request->getBody();
+ $xml = simplexml_load_string($body);
+
+ libxml_clear_errors();
+ libxml_use_internal_errors($backup);
+
+ if ($xml === false) {
+ throw new HttpBadRequestException($request, 'Invalid XML body');
+ }
+
+ $request = $request->withParsedBody($xml);
+ }
+
+ return $handler->handle($request);
+ }
+
+ private function isXmlMediaType(string $contentType): bool
+ {
+ $contentType = strtolower(trim(explode(';', $contentType)[0]));
+
+ return $contentType === 'application/xml'
+ || $contentType === 'text/xml'
+ || str_ends_with($contentType, '+xml');
+ }
+}
diff --git a/Slim/Error/Renderers/XmlExceptionRenderer.php b/Slim/Middleware/XmlExceptionMiddleware.php
similarity index 65%
rename from Slim/Error/Renderers/XmlExceptionRenderer.php
rename to Slim/Middleware/XmlExceptionMiddleware.php
index 9127405b9..c520b2fdc 100644
--- a/Slim/Error/Renderers/XmlExceptionRenderer.php
+++ b/Slim/Middleware/XmlExceptionMiddleware.php
@@ -8,39 +8,39 @@
declare(strict_types=1);
-namespace Slim\Error\Renderers;
+namespace Slim\Middleware;
use DOMDocument;
use ErrorException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamFactoryInterface;
-use Slim\Interfaces\ExceptionRendererInterface;
-use Slim\Media\MediaType;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
-use function get_class;
-
-/**
- * Formats exceptions into a XML response.
- */
-final class XmlExceptionRenderer implements ExceptionRendererInterface
+final class XmlExceptionMiddleware implements MiddlewareInterface
{
- use ExceptionRendererTrait;
+ use ExceptionMiddlewareTrait;
- private StreamFactoryInterface $streamFactory;
+ private const DEFAULT_TYPE = 'application/xml';
- public function __construct(StreamFactoryInterface $streamFactory)
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
- $this->streamFactory = $streamFactory;
+ try {
+ return $handler->handle($request);
+ } catch (Throwable $exception) {
+ $contentType = $this->detectMediaType($request);
+
+ if ($contentType === null) {
+ throw $exception;
+ }
+
+ return $this->createResponse($exception, $this->createPayload($exception), $contentType);
+ }
}
- public function __invoke(
- ServerRequestInterface $request,
- ResponseInterface $response,
- ?Throwable $exception = null,
- bool $displayErrorDetails = false,
- ): ResponseInterface {
+ private function createPayload(Throwable $exception): string
+ {
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
@@ -51,7 +51,7 @@ public function __invoke(
$errorElement->appendChild($messageElement);
// If error details should be displayed
- if ($displayErrorDetails) {
+ if ($this->displayErrorDetails) {
do {
$exceptionElement = $dom->createElement('exception');
@@ -75,9 +75,6 @@ public function __invoke(
} while ($exception = $exception->getPrevious());
}
- $body = $this->streamFactory->createStream((string)$dom->saveXML());
- $response = $response->withBody($body);
-
- return $response->withHeader('Content-Type', MediaType::APPLICATION_XML);
+ return (string)$dom->saveXML();
}
}
diff --git a/Slim/Renderers/JsonRenderer.php b/Slim/Renderers/JsonRenderer.php
deleted file mode 100644
index 4ee14a2a2..000000000
--- a/Slim/Renderers/JsonRenderer.php
+++ /dev/null
@@ -1,65 +0,0 @@
-json($response, ['key' => 'value']);
- * ```
- */
-final class JsonRenderer
-{
- private StreamFactoryInterface $streamFactory;
-
- private int $jsonOptions = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR;
-
- private string $contentType = MediaType::APPLICATION_JSON;
-
- public function __construct(StreamFactoryInterface $streamFactory)
- {
- $this->streamFactory = $streamFactory;
- }
-
- public function json(ResponseInterface $response, mixed $data = null): ResponseInterface
- {
- $response = $response->withHeader('Content-Type', $this->contentType);
- $json = (string)json_encode($data, $this->jsonOptions);
-
- return $response->withBody($this->streamFactory->createStream($json));
- }
-
- /**
- * Change the content type of the response.
- */
- public function withContentType(string $type): self
- {
- $clone = clone $this;
- $clone->contentType = $type;
-
- return $clone;
- }
-
- /**
- * Set options for JSON encoding.
- *
- * @see https://php.net/manual/function.json-encode.php
- * @see https://php.net/manual/json.constants.php
- */
- public function withJsonOptions(int $options): self
- {
- $clone = clone $this;
- $clone->jsonOptions = $options;
-
- return $clone;
- }
-}
diff --git a/tests/AppTest.php b/tests/AppTest.php
index 68d9bcbc5..1f5dca247 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -21,7 +22,6 @@
use RuntimeException;
use Slim\App;
use Slim\Builder\AppBuilder;
-use Slim\Error\Renderers\HtmlExceptionRenderer;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
use Slim\Interfaces\ContainerFactoryInterface;
@@ -30,10 +30,10 @@
use Slim\Middleware\BasePathMiddleware;
use Slim\Middleware\ContentLengthMiddleware;
use Slim\Middleware\EndpointMiddleware;
-use Slim\Middleware\ErrorHandlingMiddleware;
-use Slim\Middleware\ExceptionHandlingMiddleware;
+use Slim\Middleware\ErrorExceptionMiddleware;
use Slim\Middleware\ExceptionLoggingMiddleware;
use Slim\Middleware\HeadMethodMiddleware;
+use Slim\Middleware\HtmlExceptionMiddleware;
use Slim\Middleware\RoutingArgumentsMiddleware;
use Slim\Middleware\RoutingMiddleware;
use Slim\Psr7\Headers;
@@ -59,18 +59,17 @@ public function testAppWithExceptionAndErrorDetails(): void
$builder = new AppBuilder();
$builder->addDefinitions(
[
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
+ HtmlExceptionMiddleware::class => function ($container) {
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+ $middleware = new HtmlExceptionMiddleware($responseFactory);
- return $middleware
- ->withDisplayErrorDetails(true)
- ->withDefaultHandler(HtmlExceptionRenderer::class);
+ return $middleware->withErrorDetails(true);
},
]
);
$app = $builder->build();
- $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(HtmlExceptionMiddleware::class);
$app->add(RoutingMiddleware::class);
$app->add(EndpointMiddleware::class);
@@ -126,9 +125,9 @@ public function testAppWithMiddlewareStack(): void
$app->add(BasePathMiddleware::class);
$app->add(RoutingMiddleware::class);
$app->add(RoutingArgumentsMiddleware::class);
- $app->add(ErrorHandlingMiddleware::class);
- $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(ErrorExceptionMiddleware::class);
$app->add(ExceptionLoggingMiddleware::class);
+ $app->add(HtmlExceptionMiddleware::class);
$app->add(EndpointMiddleware::class);
$app->add(ContentLengthMiddleware::class);
diff --git a/tests/Container/DefaultDefinitionsTest.php b/tests/Container/DefaultDefinitionsTest.php
index 1e6741d22..ca27d026f 100644
--- a/tests/Container/DefaultDefinitionsTest.php
+++ b/tests/Container/DefaultDefinitionsTest.php
@@ -37,7 +37,6 @@
use Slim\Interfaces\ContainerResolverInterface;
use Slim\Interfaces\EmitterInterface;
use Slim\Interfaces\RequestHandlerInvocationStrategyInterface;
-use Slim\Middleware\ExceptionHandlingMiddleware;
use Slim\Psr7\Factory\ServerRequestFactory;
use Slim\RequestHandler\MiddlewareRequestHandler;
use Slim\Routing\Router;
@@ -159,14 +158,6 @@ public function testRequestHandlerInvocationStrategyInterface(): void
$this->assertInstanceOf(RequestResponse::class, $invocationStrategy);
}
- public function testExceptionHandlingMiddleware(): void
- {
- $container = (new AppBuilder())->build()->getContainer();
- $exceptionHandlingMiddleware = $container->get(ExceptionHandlingMiddleware::class);
-
- $this->assertInstanceOf(ExceptionHandlingMiddleware::class, $exceptionHandlingMiddleware);
- }
-
public function testLoggerInterface(): void
{
$container = (new AppBuilder())->build()->getContainer();
diff --git a/tests/Error/Renderers/HtmlExceptionFormatterTest.php b/tests/Error/Renderers/HtmlExceptionFormatterTest.php
deleted file mode 100644
index b7bff1d01..000000000
--- a/tests/Error/Renderers/HtmlExceptionFormatterTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $exception = new Exception('Test exception message');
-
- $formatter = $app->getContainer()->get(HtmlExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, true);
-
- $this->assertEquals('text/html', $result->getHeaderLine('Content-Type'));
-
- $html = (string)$result->getBody();
- $this->assertStringContainsString('Details
', $html);
- $this->assertStringContainsString('Test exception message', $html);
- $this->assertStringContainsString('Type: Exception
', $html);
- }
-
- public function testInvokeWithExceptionAndWithoutErrorDetails()
- {
- // Create the Slim app
- $app = (new AppBuilder())->build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $body = $app->getContainer()
- ->get(StreamFactoryInterface::class)
- ->createStream('');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse()
- ->withBody($body);
-
- $exception = new Exception('Test exception message');
-
- // Instantiate the formatter and invoke it
- $formatter = $app->getContainer()->get(HtmlExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, false);
-
- // Expected HTML
- $html = (string)$result->getBody();
- $this->assertStringNotContainsString('Details
', $html);
- $this->assertStringContainsString('Application Error', $html);
- $this->assertStringContainsString(
- 'A website error has occurred. Sorry for the temporary inconvenience.',
- $html
- );
- }
-}
diff --git a/tests/Error/Renderers/JsonExceptionFormatterTest.php b/tests/Error/Renderers/JsonExceptionFormatterTest.php
deleted file mode 100644
index 9ada7085f..000000000
--- a/tests/Error/Renderers/JsonExceptionFormatterTest.php
+++ /dev/null
@@ -1,107 +0,0 @@
-build();
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $exception = new Exception('Test exception message');
-
- // Instantiate the formatter with JsonRenderer and invoke it
- $formatter = $app->getContainer()->get(JsonExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, true);
-
- $this->assertEquals('application/json', $result->getHeaderLine('Content-Type'));
-
- $json = (string)$result->getBody();
- $data = json_decode($json, true);
-
- // Assertions
- $this->assertEquals('Application Error', $data['message']);
- $this->assertArrayHasKey('exception', $data);
- $this->assertCount(1, $data['exception']);
- $this->assertEquals('Test exception message', $data['exception'][0]['message']);
- }
-
- public function testInvokeWithExceptionAndWithoutErrorDetails()
- {
- $app = (new AppBuilder())->build();
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $exception = new Exception('Test exception message');
-
- $formatter = $app->getContainer()->get(JsonExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, false);
-
- $this->assertEquals('application/json', $result->getHeaderLine('Content-Type'));
-
- $json = (string)$result->getBody();
- $data = json_decode($json, true);
-
- // Assertions
- $this->assertEquals('Application Error', $data['message']);
- $this->assertArrayNotHasKey('exception', $data);
- }
-
- public function testInvokeWithHttpExceptionAndWithoutErrorDetails()
- {
- $app = (new AppBuilder())->build();
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse()
- ->withStatus(404);
-
- $exception = new HttpNotFoundException($request, 'Test exception message');
-
- $formatter = $app->getContainer()->get(JsonExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, true);
-
- $this->assertEquals('application/json', $result->getHeaderLine('Content-Type'));
-
- $json = (string)$result->getBody();
- $data = json_decode($json, true);
-
- // Assertions
- $this->assertEquals('404 Not Found', $data['message']);
- $this->assertArrayHasKey('exception', $data);
- }
-}
diff --git a/tests/Error/Renderers/PlainTextExceptionFormatterTest.php b/tests/Error/Renderers/PlainTextExceptionFormatterTest.php
deleted file mode 100644
index 8b4a2cf76..000000000
--- a/tests/Error/Renderers/PlainTextExceptionFormatterTest.php
+++ /dev/null
@@ -1,108 +0,0 @@
-build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $exception = new Exception('Test exception message');
-
- // Instantiate the formatter and invoke it
- $formatter = $app->getContainer()->get(PlainTextExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, true);
-
- // Assertions
- $this->assertEquals('text/plain', $result->getHeaderLine('Content-Type'));
-
- $text = (string)$result->getBody();
- $this->assertStringContainsString('Application Error', $text);
- $this->assertStringContainsString('Test exception message', $text);
- $this->assertStringContainsString('Type: Exception', $text);
- $this->assertStringContainsString('Message: Test exception message', $text);
- }
-
- public function testInvokeWithExceptionAndWithoutErrorDetails()
- {
- $app = (new AppBuilder())->build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $exception = new Exception('Test exception message');
-
- // Instantiate the formatter and invoke it
- $formatter = $app->getContainer()->get(PlainTextExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, false);
-
- // Assertions
- $this->assertEquals('text/plain', $result->getHeaderLine('Content-Type'));
-
- $text = (string)$result->getBody();
- $this->assertStringContainsString('Application Error', $text);
- $this->assertStringNotContainsString('Test exception message', $text);
- $this->assertStringNotContainsString('Type: Exception', $text);
- }
-
- public function testInvokeWithNestedExceptionsAndWithErrorDetails()
- {
- $app = (new AppBuilder())->build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $innerException = new Exception('Inner exception message');
- $outerException = new Exception('Outer exception message', 0, $innerException);
-
- // Instantiate the formatter and invoke it
- $formatter = $app->getContainer()->get(PlainTextExceptionRenderer::class);
- $result = $formatter($request, $response, $outerException, true);
-
- // Assertions
- $this->assertEquals('text/plain', $result->getHeaderLine('Content-Type'));
-
- $text = (string)$result->getBody();
- $this->assertStringContainsString('Application Error', $text);
- $this->assertStringContainsString('Outer exception message', $text);
- $this->assertStringContainsString('Inner exception message', $text);
- $this->assertStringContainsString('Previous Exception:', $text);
- }
-}
diff --git a/tests/Error/Renderers/XmlExceptionFormatterTest.php b/tests/Error/Renderers/XmlExceptionFormatterTest.php
deleted file mode 100644
index 1137ecae4..000000000
--- a/tests/Error/Renderers/XmlExceptionFormatterTest.php
+++ /dev/null
@@ -1,108 +0,0 @@
-build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $exception = new Exception('Test exception message');
-
- // Instantiate the formatter and invoke it
- $formatter = $app->getContainer()->get(XmlExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, true);
-
- // Assertions
- $this->assertEquals('application/xml', $result->getHeaderLine('Content-Type'));
-
- $xml = (string)$result->getBody();
- $this->assertStringContainsString('Application Error', $xml);
- $this->assertStringContainsString('', $xml);
- $this->assertStringContainsString('Exception', $xml);
- $this->assertStringContainsString('Test exception message', $xml);
- }
-
- public function testInvokeWithExceptionAndWithoutErrorDetails()
- {
- $app = (new AppBuilder())->build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $exception = new Exception('Test exception message');
-
- // Instantiate the formatter and invoke it
- $formatter = $app->getContainer()->get(XmlExceptionRenderer::class);
- $result = $formatter($request, $response, $exception, false);
-
- // Assertions
- $this->assertEquals('application/xml', $result->getHeaderLine('Content-Type'));
-
- $xml = (string)$result->getBody();
- $this->assertStringContainsString('Application Error', $xml);
- $this->assertStringNotContainsString('', $xml);
- $this->assertStringNotContainsString('Exception', $xml);
- }
-
- public function testInvokeWithNestedExceptionsAndWithErrorDetails()
- {
- $app = (new AppBuilder())->build();
-
- // Create a request and response
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $response = $app->getContainer()
- ->get(ResponseFactoryInterface::class)
- ->createResponse();
-
- $innerException = new Exception('Inner exception message');
- $outerException = new Exception('Outer exception message', 0, $innerException);
-
- // Instantiate the formatter and invoke it
- $formatter = $app->getContainer()->get(XmlExceptionRenderer::class);
- $result = $formatter($request, $response, $outerException, true);
-
- // Assertions
- $this->assertEquals('application/xml', $result->getHeaderLine('Content-Type'));
-
- $xml = (string)$result->getBody();
- $this->assertStringContainsString('Application Error', $xml);
- $this->assertStringContainsString('', $xml);
- $this->assertStringContainsString('Outer exception message', $xml);
- $this->assertStringContainsString('Inner exception message', $xml);
- }
-}
diff --git a/tests/Media/MediaTypeDetectorTest.php b/tests/Media/MediaTypeDetectorTest.php
deleted file mode 100644
index 6449715f7..000000000
--- a/tests/Media/MediaTypeDetectorTest.php
+++ /dev/null
@@ -1,74 +0,0 @@
-build();
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/')
- ->withHeader('Accept', $acceptHeader);
-
- $mediaTypeDetector = new MediaTypeDetector();
- $detectedMediaTypes = $mediaTypeDetector->detect($request);
-
- $this->assertEquals($expectedMediaTypes, $detectedMediaTypes);
- }
-
- #[DataProvider('provideContentTypeCases')]
- public function testDetectFromContentTypeHeader(string $contentTypeHeader, array $expectedMediaTypes)
- {
- $app = (new AppBuilder())->build();
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('POST', '/')
- ->withHeader('Content-Type', $contentTypeHeader);
-
- $mediaTypeDetector = new MediaTypeDetector();
- $detectedMediaTypes = $mediaTypeDetector->detect($request);
-
- $this->assertEquals($expectedMediaTypes, $detectedMediaTypes);
- }
-
- public static function provideAcceptHeaderCases(): array
- {
- return [
- ['application/json', [0 => 'application/json']],
- ['text/html', [0 => 'text/html']],
- ['application/xml, text/html', [0 => 'application/xml', 1 => 'text/html']],
- ['*/*', [0 => '*/*']],
- ['', []],
- ];
- }
-
- public static function provideContentTypeCases(): array
- {
- return [
- ['application/json', [0 => 'application/json']],
- ['text/html', [0 => 'text/html']],
- ['application/xml; charset=UTF-8', [0 => 'application/xml']],
- ['application/vnd.api+json', [0 => 'application/vnd.api+json']],
- ['', []],
- ];
- }
-}
diff --git a/tests/Middleware/BodyParsingMiddlewareTest.php b/tests/Middleware/BodyParsingMiddlewareTest.php
deleted file mode 100644
index a0aaadc49..000000000
--- a/tests/Middleware/BodyParsingMiddlewareTest.php
+++ /dev/null
@@ -1,361 +0,0 @@
-addDefinitions(
- [
- BodyParsingMiddleware::class => function (ContainerInterface $container) {
- $mediaTypeDetector = $container->get(MediaTypeDetector::class);
- $middleware = new BodyParsingMiddleware($mediaTypeDetector);
-
- return $middleware
- ->withDefaultMediaType('text/html')
- ->withDefaultBodyParsers();
- },
- ]
- );
-
- $builder->addDefinitionsClass(NyholmDefinitions::class);
- $app = $builder->build();
-
- $responseFactory = $app->getContainer()->get(ResponseFactoryMiddleware::class);
-
- $test = $this;
- $middlewares = [
- $app->getContainer()->get(BodyParsingMiddleware::class),
- $this->createCallbackMiddleware(function (ServerRequestInterface $request) use ($expected, $test) {
- $test->assertEquals($expected, $request->getParsedBody());
- }),
- $responseFactory,
- ];
-
- $stream = $app->getContainer()
- ->get(StreamFactoryInterface::class)
- ->createStream($body);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('POST', '/')
- ->withHeader('Accept', $contentType)
- ->withHeader('Content-Type', $contentType)
- ->withBody($stream);
-
- (new Runner($middlewares))->handle($request);
- }
-
- public static function parsingProvider(): array
- {
- return [
- 'form' => [
- 'application/x-www-form-urlencoded;charset=utf8',
- 'foo=bar',
- ['foo' => 'bar'],
- ],
- 'json' => [
- 'application/json',
- '{"foo":"bar"}',
- ['foo' => 'bar'],
- ],
- 'json-with-charset' => [
- "application/json\t ; charset=utf8",
- '{"foo":"bar"}',
- ['foo' => 'bar'],
- ],
- 'json-suffix' => [
- 'application/vnd.api+json;charset=utf8',
- '{"foo":"bar"}',
- ['foo' => 'bar'],
- ],
- 'xml' => [
- 'application/xml',
- 'John',
- simplexml_load_string('John'),
- ],
- 'text-xml' => [
- 'text/xml',
- 'John',
- simplexml_load_string('John'),
- ],
- 'valid-json-but-not-an-array' => [
- 'application/json;charset=utf8',
- '"foo bar"',
- null,
- ],
- 'unknown-contenttype' => [
- 'text/foo+bar',
- '"foo bar"',
- null,
- ],
- 'empty-contenttype' => [
- '',
- '"foo bar"',
- null,
- ],
- // null is not supported anymore
- // 'no-contenttype' => [
- // null,
- // '"foo bar"',
- // null,
- // ],
- 'invalid-contenttype' => [
- 'foo',
- '"foo bar"',
- null,
- ],
- 'invalid-xml' => [
- 'application/xml',
- 'John',
- null,
- ],
- 'invalid-textxml' => [
- 'text/xml',
- 'John',
- null,
- ],
- ];
- }
-
- #[DataProvider('parsingInvalidJsonProvider')]
- public function testParsingInvalidJsonWithSlimPsr7($contentType, $body)
- {
- $builder = new AppBuilder();
- $builder->addDefinitions(
- [
- BodyParsingMiddleware::class => function (ContainerInterface $container) {
- $mediaTypeDetector = $container->get(MediaTypeDetector::class);
- $middleware = new BodyParsingMiddleware($mediaTypeDetector);
-
- return $middleware
- ->withDefaultMediaType('text/html')
- ->withDefaultBodyParsers();
- },
- ]
- );
-
- $builder->addDefinitionsClass(SlimPsr7Definitions::class);
- $app = $builder->build();
- $container = $app->getContainer();
-
- $middlewares = [
- $container->get(BodyParsingMiddleware::class),
- $container->get(ResponseFactoryMiddleware::class),
- ];
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('POST', '/')
- ->withHeader('Accept', $contentType)
- ->withHeader('Content-Type', $contentType);
-
- $request->getBody()->write($body);
-
- $response = (new Runner($middlewares))->handle($request);
-
- $this->assertSame('', (string)$response->getBody());
- }
-
- public static function parsingInvalidJsonProvider(): array
- {
- return [
- 'invalid-json' => [
- 'application/json;charset=utf8',
- '{"foo"}/bar',
- ],
- 'invalid-json-2' => [
- 'application/json',
- '{',
- ],
- ];
- }
-
- public function testParsingWithARegisteredParserAndSlimHttp()
- {
- $builder = new AppBuilder();
-
- // Replace or change the PSR-17 factory because slim/http has its own parser
- $builder->addDefinitionsClass(SlimHttpDefinitions::class);
- $builder->addDefinitions(
- [
- BodyParsingMiddleware::class => function (ContainerInterface $container) {
- $mediaTypeDetector = $container->get(MediaTypeDetector::class);
- $middleware = new BodyParsingMiddleware($mediaTypeDetector);
-
- return $middleware->withBodyParser('application/vnd.api+json', function ($input) {
- return ['data' => json_decode($input, true)];
- });
- },
- ]
- );
- $app = $builder->build();
-
- $input = '{"foo":"bar"}';
- $stream = $app->getContainer()
- ->get(StreamFactoryInterface::class)
- ->createStream($input);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('POST', '/')
- ->withHeader('Accept', 'application/vnd.api+json;charset=utf8')
- ->withBody($stream);
-
- $middlewares = [];
- $middlewares[] = $app->getContainer()->get(BodyParsingMiddleware::class);
- $middlewares[] = $this->createParsedBodyMiddleware();
- $middlewares[] = $app->getContainer()->get(ResponseFactoryMiddleware::class);
-
- $response = (new Runner($middlewares))->handle($request);
-
- $this->assertJsonResponse(['data' => ['foo' => 'bar']], $response);
- $this->assertSame(['data' => ['foo' => 'bar']], json_decode((string)$response->getBody(), true));
- }
-
- #[DataProvider('httpDefinitionsProvider')]
- public function testParsingFailsWhenAnInvalidTypeIsReturned(string $definitions)
- {
- // The slim/http package has its own body parser, so this middleware will not be used.
- // The SlimHttpDefinitions::class will not fail here, because the body parser will not be executed.
- if ($definitions === SlimHttpDefinitions::class) {
- $this->assertTrue(true);
-
- return;
- }
-
- $this->expectException(RuntimeException::class);
-
- $builder = new AppBuilder();
- $builder->addDefinitionsClass($definitions);
-
- $builder->addDefinitions(
- [
- BodyParsingMiddleware::class => function (ContainerInterface $container) {
- $mediaTypeDetector = $container->get(MediaTypeDetector::class);
- $middleware = new BodyParsingMiddleware($mediaTypeDetector);
-
- $middleware = $middleware->withBodyParser('application/json', function () {
- // invalid - should return null, array or object
- return 10;
- });
-
- return $middleware;
- },
- ]
- );
- $app = $builder->build();
-
- $stream = $app->getContainer()
- ->get(StreamFactoryInterface::class)
- ->createStream('{"foo":"bar"}');
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('POST', '/')
- ->withHeader('Accept', 'application/json;charset=utf8')
- ->withHeader('Content-Type', 'application/json;charset=utf8')
- ->withBody($stream);
-
- $middlewares = [];
- $middlewares[] = $app->getContainer()->get(BodyParsingMiddleware::class);
- $middlewares[] = $this->createParsedBodyMiddleware();
- $middlewares[] = $app->getContainer()->get(ResponseFactoryMiddleware::class);
-
- (new Runner($middlewares))->handle($request);
- }
-
- public static function httpDefinitionsProvider(): array
- {
- return [
- 'GuzzleDefinitions' => [GuzzleDefinitions::class],
- 'HttpSoftDefinitions' => [HttpSoftDefinitions::class],
- 'LaminasDiactorosDefinitions' => [LaminasDiactorosDefinitions::class],
- 'NyholmDefinitions' => [NyholmDefinitions::class],
- 'SlimHttpDefinitions' => [SlimHttpDefinitions::class],
- 'SlimPsr7Definitions' => [SlimPsr7Definitions::class],
- ];
- }
-
- private function createParsedBodyMiddleware(): MiddlewareInterface
- {
- return new class implements MiddlewareInterface {
- public function process(
- ServerRequestInterface $request,
- RequestHandlerInterface $handler,
- ): ResponseInterface {
- $response = $handler->handle($request);
-
- // Return the parsed body
- $response->getBody()->write(json_encode($request->getParsedBody()));
-
- return $response;
- }
- };
- }
-
- private function createCallbackMiddleware(callable $callback): MiddlewareInterface
- {
- return new class ($callback) implements MiddlewareInterface {
- /**
- * @var callable
- */
- private $callback;
-
- public function __construct(callable $callback)
- {
- $this->callback = $callback;
- }
-
- public function process(
- ServerRequestInterface $request,
- RequestHandlerInterface $handler,
- ): ResponseInterface {
- $response = $handler->handle($request);
-
- call_user_func($this->callback, $request, $handler);
-
- return $response;
- }
- };
- }
-}
diff --git a/tests/Middleware/ErrorHandlingMiddlewareTest.php b/tests/Middleware/ErrorExceptionMiddlewareTest.php
similarity index 90%
rename from tests/Middleware/ErrorHandlingMiddlewareTest.php
rename to tests/Middleware/ErrorExceptionMiddlewareTest.php
index 19fd5a7bf..3935d9176 100644
--- a/tests/Middleware/ErrorHandlingMiddlewareTest.php
+++ b/tests/Middleware/ErrorExceptionMiddlewareTest.php
@@ -19,10 +19,10 @@
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Builder\AppBuilder;
use Slim\Middleware\EndpointMiddleware;
-use Slim\Middleware\ErrorHandlingMiddleware;
+use Slim\Middleware\ErrorExceptionMiddleware;
use Slim\Middleware\RoutingMiddleware;
-final class ErrorHandlingMiddlewareTest extends TestCase
+final class ErrorExceptionMiddlewareTest extends TestCase
{
public function testProcessHandlesError(): void
{
@@ -45,7 +45,7 @@ public function testProcessHandlesError(): void
$app = (new AppBuilder())->build();
$middleware = $app
->getContainer()
- ->get(ErrorHandlingMiddleware::class);
+ ->get(ErrorExceptionMiddleware::class);
// Invoke the middleware process method
$middleware->process($request, $handler);
@@ -59,7 +59,7 @@ public function testProcessHandlesErrorSilent(): void
$app = $builder->build();
$middleware = $app
->getContainer()
- ->get(ErrorHandlingMiddleware::class);
+ ->get(ErrorExceptionMiddleware::class);
$app->add($middleware);
$app->add(RoutingMiddleware::class);
@@ -97,7 +97,7 @@ public function testProcessHandlesException(): void
// Instantiate the middleware
$app = (new AppBuilder())->build();
- $middleware = $app->getContainer()->get(ErrorHandlingMiddleware::class);
+ $middleware = $app->getContainer()->get(ErrorExceptionMiddleware::class);
// Invoke the middleware process method
$middleware->process($request, $handler);
@@ -115,7 +115,7 @@ public function testProcessReturnsResponse(): void
->willReturn($response);
$app = (new AppBuilder())->build();
- $middleware = $app->getContainer()->get(ErrorHandlingMiddleware::class);
+ $middleware = $app->getContainer()->get(ErrorExceptionMiddleware::class);
// Invoke the middleware process method and assert the response is returned
$result = $middleware->process($request, $handler);
diff --git a/tests/Middleware/ExceptionHandlingMiddlewareTest.php b/tests/Middleware/ExceptionHandlingMiddlewareTest.php
deleted file mode 100644
index f4de371ad..000000000
--- a/tests/Middleware/ExceptionHandlingMiddlewareTest.php
+++ /dev/null
@@ -1,356 +0,0 @@
-addDefinitions(
- [
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
-
- return $middleware
- ->withDisplayErrorDetails(false)
- ->withDefaultHandler(HtmlExceptionRenderer::class);
- },
- ]
- );
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $app->get('/', function () {
- throw new RuntimeException('Test error');
- });
-
- $response = $app->handle($request);
-
- $this->assertSame(500, $response->getStatusCode());
- $this->assertSame('text/html', $response->getHeaderLine('Content-Type'));
- $this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
- $this->assertStringContainsString('Application Error
', (string)$response->getBody());
- }
-
- public function testDefaultHandlerWithDetails(): void
- {
- $builder = new AppBuilder();
- $builder->addDefinitions(
- [
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
-
- return $middleware
- ->withDisplayErrorDetails(true)
- ->withDefaultHandler(HtmlExceptionRenderer::class);
- },
- ]
- );
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $app->get('/', function () {
- throw new RuntimeException('Test error', 123);
- });
-
- $response = $app->handle($request);
-
- $this->assertSame(500, $response->getStatusCode());
- $this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
- $this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
- $this->assertStringContainsString('Application Error
', (string)$response->getBody());
- }
-
- public function testDefaultHtmlMediaTypeWithDetails(): void
- {
- $builder = new AppBuilder();
- $builder->addDefinitions(
- [
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
-
- return $middleware
- ->withDisplayErrorDetails(true)
- ->withDefaultMediaType(MediaType::TEXT_HTML)
- ->withHandler(MediaType::TEXT_HTML, HtmlExceptionRenderer::class);
- },
- ]
- );
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/')
- ->withHeader('Accept', 'application/json');
-
- $app->get('/', function () {
- throw new RuntimeException('Test error', 123);
- });
-
- $response = $app->handle($request);
-
- $this->assertSame(500, $response->getStatusCode());
- $this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
- $this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
- $this->assertStringContainsString('Application Error
', (string)$response->getBody());
- }
-
- public function testJsonMediaTypeDisplayErrorDetails(): void
- {
- $builder = new AppBuilder();
-
- $builder->addDefinitions(
- [
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
-
- return $middleware
- ->withDisplayErrorDetails(true)
- ->withHandler(MediaType::APPLICATION_JSON, JsonExceptionRenderer::class);
- },
- ]
- );
-
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/')
- ->withHeader('Accept', 'application/json');
-
- $app->get('/', function () {
- throw new RuntimeException('Test error', 123);
- });
-
- $response = $app->handle($request);
-
- $actual = json_decode((string)$response->getBody(), true);
- $this->assertSame('Application Error', $actual['message']);
- $this->assertSame(1, count($actual['exception']));
- $this->assertSame('RuntimeException', $actual['exception'][0]['type']);
- $this->assertSame(123, $actual['exception'][0]['code']);
- $this->assertSame('Test error', $actual['exception'][0]['message']);
- }
-
- public function testWithoutHandler(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Exception handler for "text/html" not found');
-
- $builder = new AppBuilder();
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/');
-
- $app->get('/', function () {
- throw new RuntimeException('Test error', 123);
- });
-
- $app->handle($request);
- }
-
- #[DataProvider('textHmlHeaderProvider')]
- public function testWithTextHtml(string $header, string $headerValue): void
- {
- $builder = new AppBuilder();
- $builder->addDefinitions(
- [
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
-
- return $middleware
- ->withDisplayErrorDetails(true)
- ->withHandler(MediaType::TEXT_HTML, HtmlExceptionRenderer::class);
- },
- ]
- );
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $app->get('/', function () {
- throw new RuntimeException('Test Error message');
- });
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/')
- ->withHeader($header, $headerValue);
-
- $response = $app->handle($request);
-
- $this->assertSame(500, $response->getStatusCode());
- $this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
- $this->assertStringContainsString('Test Error message', (string)$response->getBody());
- }
-
- public static function textHmlHeaderProvider(): array
- {
- return [
- ['Accept', 'text/html'],
- ['Accept', 'text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8'],
- ['Content-Type', 'text/html'],
- ['Content-Type', 'text/html; charset=utf-8'],
- ];
- }
-
- // todo: Add test for other media types
-
- public function testWithAcceptJson(): void
- {
- $builder = new AppBuilder();
- $builder->addDefinitions(
- [
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
-
- return $middleware
- ->withDisplayErrorDetails(false)
- ->withHandler(MediaType::APPLICATION_JSON, JsonExceptionRenderer::class);
- },
- ]
- );
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/')
- ->withHeader('Accept', 'application/json');
-
- $app->get('/', function () {
- throw new RuntimeException('Test exception');
- });
-
- $response = $app->handle($request);
-
- $this->assertSame(500, $response->getStatusCode());
- $expected = [
- 'message' => 'Application Error',
- ];
- $this->assertJsonResponse($expected, $response);
- }
-
- public static function xmlHeaderProvider(): array
- {
- return [
- ['Accept', 'application/xml'],
- ['Accept', 'application/xml, application/json'],
- ['Content-Type', 'application/xml'],
- ['Content-Type', 'application/xml; charset=utf-8'],
- ];
- }
-
- #[DataProvider('xmlHeaderProvider')]
- public function testWithAcceptXml(string $header, string $headerValue): void
- {
- $builder = new AppBuilder();
- $builder->addDefinitions(
- [
- ExceptionHandlingMiddleware::class => function ($container) {
- $middleware = ExceptionHandlingMiddleware::createFromContainer($container);
-
- return $middleware->withDisplayErrorDetails(false)
- ->withoutHandlers()
- ->withHandler('application/json', JsonExceptionRenderer::class)
- ->withHandler('application/xml', XmlExceptionRenderer::class);
- },
- ]
- );
- $app = $builder->build();
-
- $app->add(ExceptionHandlingMiddleware::class);
- $app->add(RoutingMiddleware::class);
- $app->add(EndpointMiddleware::class);
-
- $request = $app->getContainer()
- ->get(ServerRequestFactoryInterface::class)
- ->createServerRequest('GET', '/')
- ->withHeader($header, $headerValue);
-
- $app->get('/', function () {
- throw new RuntimeException('Test exception');
- });
-
- $response = $app->handle($request);
-
- $this->assertSame(500, $response->getStatusCode());
- $expected = '
-
- Application Error
- ';
-
- $dom = new DOMDocument();
- $dom->preserveWhiteSpace = false;
- $dom->formatOutput = true;
- $dom->loadXML($expected);
- $expected = $dom->saveXML();
-
- $dom2 = new DOMDocument();
- $dom2->preserveWhiteSpace = false;
- $dom2->formatOutput = true;
- $dom2->loadXML((string)$response->getBody());
- $actual = $dom2->saveXML();
-
- $this->assertSame($expected, $actual);
- }
-}
diff --git a/tests/Middleware/ExceptionLoggingMiddlewareTest.php b/tests/Middleware/ExceptionLoggingMiddlewareTest.php
index f356740e4..53b6297ee 100644
--- a/tests/Middleware/ExceptionLoggingMiddlewareTest.php
+++ b/tests/Middleware/ExceptionLoggingMiddlewareTest.php
@@ -20,7 +20,7 @@
use RuntimeException;
use Slim\Builder\AppBuilder;
use Slim\Middleware\EndpointMiddleware;
-use Slim\Middleware\ErrorHandlingMiddleware;
+use Slim\Middleware\ErrorExceptionMiddleware;
use Slim\Middleware\ExceptionLoggingMiddleware;
use Slim\Middleware\RoutingMiddleware;
use Slim\Tests\Logging\TestLogger;
@@ -113,7 +113,7 @@ public function testUserLevelErrorIsLogged(): void
error_reporting(E_ALL);
$logger = new TestLogger();
- $app->add(ErrorHandlingMiddleware::class);
+ $app->add(ErrorExceptionMiddleware::class);
$middleware = new ExceptionLoggingMiddleware($logger);
diff --git a/tests/Middleware/FormUrlEncodedBodyParserMiddlewareTest.php b/tests/Middleware/FormUrlEncodedBodyParserMiddlewareTest.php
new file mode 100644
index 000000000..f74259403
--- /dev/null
+++ b/tests/Middleware/FormUrlEncodedBodyParserMiddlewareTest.php
@@ -0,0 +1,100 @@
+createStream($body);
+
+ $request = (new ServerRequestFactory())
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', 'application/x-www-form-urlencoded')
+ ->withBody($stream);
+
+ $middleware = new FormUrlEncodedBodyParserMiddleware();
+ $response = $middleware->process($request, new class implements RequestHandlerInterface {
+ public function handle(
+ ServerRequestInterface $request
+ ): ResponseInterface {
+ $parsed = $request->getParsedBody();
+ $response = new Response();
+ $response->getBody()->write($parsed['foo'] . ',' . $parsed['baz']);
+
+ return $response;
+ }
+ });
+
+ $this->assertSame('bar,qux', (string)$response->getBody());
+ }
+
+ public function testSkipsParsingForNonFormContentType(): void
+ {
+ $stream = (new StreamFactory())->createStream('foo=bar&baz=qux');
+
+ $request = (new ServerRequestFactory())
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', 'text/plain')
+ ->withBody($stream);
+
+ $middleware = new FormUrlEncodedBodyParserMiddleware();
+ $response = $middleware->process($request, new class implements RequestHandlerInterface {
+ public function handle(
+ ServerRequestInterface $request
+ ): ResponseInterface {
+ $parsed = $request->getParsedBody();
+ $response = new Response();
+ $response->getBody()->write($parsed === null ? 'no-parse' : 'parsed');
+
+ return $response;
+ }
+ });
+
+ $this->assertSame('no-parse', (string)$response->getBody());
+ }
+
+ public function testSkipsParsingForEmptyBody(): void
+ {
+ $stream = (new StreamFactory())->createStream('');
+
+ $request = (new ServerRequestFactory())
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', 'application/x-www-form-urlencoded')
+ ->withBody($stream);
+
+ $middleware = new FormUrlEncodedBodyParserMiddleware();
+ $response = $middleware->process($request, new class implements RequestHandlerInterface {
+ public function handle(
+ ServerRequestInterface $request
+ ): ResponseInterface {
+ $parsed = $request->getParsedBody();
+ $response = new Response();
+ $response->getBody()->write(json_encode($parsed));
+
+ return $response;
+ }
+ });
+
+ // empty
+ $this->assertSame('[]', (string)$response->getBody());
+ }
+}
diff --git a/tests/Middleware/HtmlExceptionMiddlewareTest.php b/tests/Middleware/HtmlExceptionMiddlewareTest.php
new file mode 100644
index 000000000..c68f41fd0
--- /dev/null
+++ b/tests/Middleware/HtmlExceptionMiddlewareTest.php
@@ -0,0 +1,114 @@
+createServerRequest('GET', '/');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return (new ResponseFactory())->createResponse();
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(200, $response->getStatusCode());
+ }
+
+ public function testProcessCatchesGenericExceptionWithHtmlAccept(): void
+ {
+ $middleware = (new HtmlExceptionMiddleware(new ResponseFactory()))
+ ->withMimeType('text/html');
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'text/html');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('Generic failure');
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertStringContainsString('Application Error', (string)$response->getBody());
+ }
+
+ public function testProcessWithHttpMethodNotAllowedAddsAllowHeader(): void
+ {
+ $middleware = (new HtmlExceptionMiddleware(new ResponseFactory()))
+ ->withMimeType('text/html');
+
+ $request = (new ServerRequestFactory())->createServerRequest('POST', '/')
+ ->withHeader('Accept', 'text/html');
+
+ $handler = new class ($request) implements RequestHandlerInterface {
+ private $request;
+
+ public function __construct($request)
+ {
+ $this->request = $request;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $exception = new HttpMethodNotAllowedException($this->request);
+ $exception->setAllowedMethods(['GET', 'PUT']);
+
+ throw $exception;
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('GET, PUT', $response->getHeaderLine('Allow'));
+ }
+
+ public function testProcessThrowsOriginalExceptionIfMediaTypeNotAccepted(): void
+ {
+ $middleware = new HtmlExceptionMiddleware(new ResponseFactory());
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/xml');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('Unsupported type');
+ }
+ };
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Unsupported type');
+
+ $middleware->process($request, $handler);
+ }
+}
diff --git a/tests/Middleware/JsonBodyParserMiddlewareTest.php b/tests/Middleware/JsonBodyParserMiddlewareTest.php
new file mode 100644
index 000000000..30ebb008f
--- /dev/null
+++ b/tests/Middleware/JsonBodyParserMiddlewareTest.php
@@ -0,0 +1,201 @@
+createStream($body);
+
+ $request = (new ServerRequestFactory())
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', $contentType)
+ ->withBody($stream);
+
+ $middleware = new JsonBodyParserMiddleware();
+
+ $response = $middleware->process($request, new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $data = $request->getParsedBody();
+ $response = new Response();
+ $response->getBody()->write(json_encode($data));
+
+ return $response;
+ }
+ });
+
+ $this->assertSame($expected, (string)$response->getBody());
+ }
+
+ public function testParsesStructuredJsonType(): void
+ {
+ $stream = (new StreamFactory())->createStream('{"hello":"world"}');
+
+ $request = (new ServerRequestFactory())
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', 'application/vnd.api+json')
+ ->withBody($stream);
+
+ $middleware = new JsonBodyParserMiddleware();
+
+ $response = $middleware->process($request, new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $data = $request->getParsedBody();
+ $response = new Response();
+ $response->getBody()->write(json_encode($data));
+
+ return $response;
+ }
+ });
+
+ $this->assertSame('{"hello":"world"}', (string)$response->getBody());
+ }
+
+ #[DataProvider('invalidJsonProvider')]
+ public function testThrowsExceptionOnInvalidJson($contentType, $body): void
+ {
+ $this->expectException(JsonException::class);
+ $this->expectExceptionMessage('Syntax error');
+
+ $stream = (new StreamFactory())->createStream('{"foo": "bar"');
+
+ $request = (new ServerRequestFactory())
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', 'application/json')
+ ->withBody($stream);
+
+ $middleware = new JsonBodyParserMiddleware();
+
+ $middleware->process($request, new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return new Response();
+ }
+ });
+ }
+
+ public function testSkipsParsingForNonJsonContentType(): void
+ {
+ $stream = (new StreamFactory())->createStream('{"foo":"bar"}');
+
+ $request = (new ServerRequestFactory())
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', 'text/plain')
+ ->withBody($stream);
+
+ $middleware = new JsonBodyParserMiddleware();
+
+ $response = $middleware->process($request, new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $response = new Response();
+ $parsedBody = $request->getParsedBody();
+ $response->getBody()->write($parsedBody === null ? 'no-parse' : 'parsed');
+
+ return $response;
+ }
+ });
+
+ $this->assertSame('no-parse', (string)$response->getBody());
+ }
+
+ public static function validJsonProvider(): array
+ {
+ return [
+ 'json' => [
+ 'application/json',
+ '{"foo":"bar"}',
+ '{"foo":"bar"}',
+ ],
+ 'json-with-charset' => [
+ "application/json\t ; charset=utf8",
+ '{"foo":"bar"}',
+ '{"foo":"bar"}',
+ ],
+ 'json-suffix' => [
+ 'application/vnd.api+json;charset=utf8',
+ '{"foo":"bar"}',
+ '{"foo":"bar"}',
+ ],
+ 'valid-json-but-not-an-array' => [
+ 'application/json;charset=utf8',
+ '"foo bar"',
+ 'null',
+ ],
+ 'empty-object' => [
+ 'application/json',
+ '{}',
+ '[]',
+ ],
+ 'unknown-contenttype' => [
+ 'text/foo+bar',
+ '"foo bar"',
+ 'null',
+ ],
+ 'empty-contenttype' => [
+ '',
+ '"foo bar"',
+ 'null',
+ ],
+ // null is not supported anymore
+ // Header values must be RFC 7230 compatible strings.
+ /* 'no-contenttype' => [
+ null,
+ '"foo bar"',
+ 'null',
+ ],*/
+ 'json-null' => [
+ 'application/json',
+ 'null',
+ 'null',
+ ],
+ 'json-false' => [
+ 'application/json',
+ 'false',
+ 'null',
+ ],
+ 'invalid-contenttype' => [
+ 'foo',
+ '"foo bar"',
+ 'null',
+ ],
+ ];
+ }
+
+ public static function invalidJsonProvider(): array
+ {
+ return [
+ 'invalid-json' => [
+ 'application/json',
+ '{"foo": "bar"',
+ ],
+ 'invalid-json-empty-string' => [
+ 'application/json',
+ '',
+ ],
+ ];
+ }
+}
diff --git a/tests/Middleware/JsonExceptionMiddlewareTest.php b/tests/Middleware/JsonExceptionMiddlewareTest.php
new file mode 100644
index 000000000..39a88df6c
--- /dev/null
+++ b/tests/Middleware/JsonExceptionMiddlewareTest.php
@@ -0,0 +1,224 @@
+createServerRequest('GET', '/');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return (new ResponseFactory())->createResponse();
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(200, $response->getStatusCode());
+ }
+
+ public function testProcessCatchesExceptionWithJsonAccept(): void
+ {
+ $middleware = (new JsonExceptionMiddleware(new ResponseFactory()))
+ ->withMimeType('application/json')
+ ->withErrorDetails(true);
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/json');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('Something went wrong');
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
+ $this->assertJson((string)$response->getBody());
+ $this->assertStringContainsString('Something went wrong', (string)$response->getBody());
+ }
+
+ public function testProcessWithHttpMethodNotAllowedIncludesAllowHeader(): void
+ {
+ $middleware = (new JsonExceptionMiddleware(new ResponseFactory()))
+ ->withMimeType('application/json');
+
+ $request = (new ServerRequestFactory())->createServerRequest('POST', '/')
+ ->withHeader('Accept', 'application/json');
+
+ $handler = new class ($request) implements RequestHandlerInterface {
+ private $request;
+
+ public function __construct($request)
+ {
+ $this->request = $request;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $exception = new HttpMethodNotAllowedException($this->request);
+ $exception->setAllowedMethods(['GET', 'PATCH']);
+ throw $exception;
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('GET, PATCH', $response->getHeaderLine('Allow'));
+ }
+
+ public function testWithErrorDetails(): void
+ {
+ $middleware = (new JsonExceptionMiddleware(new ResponseFactory()))
+ ->withMimeType('application/json')
+ ->withErrorDetails(true);
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/json');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('Detailed error');
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(500, $response->getStatusCode());
+
+ $body = (string)$response->getBody();
+ $this->assertJson($body);
+ $this->assertStringContainsString('exception', $body);
+ $this->assertStringContainsString('Application Error', $body);
+ $this->assertStringContainsString('Detailed error', $body);
+
+ $data = json_decode($body, true);
+
+ $this->assertSame('Application Error', $data['message']);
+ $this->assertArrayHasKey('exception', $data);
+ $this->assertSame(1, count($data['exception']));
+ $this->assertSame('Detailed error', $data['exception'][0]['message']);
+ $this->assertSame(['type', 'code', 'message', 'file', 'line'], array_keys($data['exception'][0]));
+ }
+
+ public function testWithoutErrorDetails(): void
+ {
+ $middleware = (new JsonExceptionMiddleware(new ResponseFactory()))
+ ->withMimeType('application/json')
+ ->withErrorDetails(false);
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/json');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('Hidden error');
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertJson((string)$response->getBody());
+ $this->assertStringNotContainsString('exception', (string)$response->getBody());
+ $this->assertStringContainsString('Application Error', (string)$response->getBody());
+ $this->assertStringNotContainsString('Hidden error', (string)$response->getBody());
+ }
+
+ public function testRethrowsExceptionWhenNoAcceptableContentTypeDetected(): void
+ {
+ $middleware = (new JsonExceptionMiddleware(new ResponseFactory()));
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/unsupported-type');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('This should be rethrown');
+ }
+ };
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('This should be rethrown');
+
+ $middleware->process($request, $handler);
+ }
+
+ public function testWithMimeTypeSupportsAdditionalJsonTypes(): void
+ {
+ $middleware = (new JsonExceptionMiddleware(new ResponseFactory()))
+ ->withErrorDetails(true)
+ ->withMimeType('application/vnd.api+json')
+ ->withMimeType('application/ld+json');
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/vnd.api+json');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('Test message');
+ }
+ };
+
+ $response = $middleware->process($request, $handler);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('application/vnd.api+json', $response->getHeaderLine('Content-Type'));
+ $this->assertJson((string)$response->getBody());
+ $this->assertStringContainsString('Test message', (string)$response->getBody());
+ }
+
+ public function testWithJsonOptionsChangesEncoding(): void
+ {
+ $middleware = (new JsonExceptionMiddleware(new ResponseFactory()))
+ ->withErrorDetails(true)
+ ->withMimeType('application/json')
+ ->withJsonOptions(JSON_HEX_TAG);
+
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/json');
+
+ $handler = new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new RuntimeException('