From 101878ba057d30b90fba881524dfc50bc59b048e Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Thu, 5 Mar 2026 22:48:27 +0100 Subject: [PATCH] feat(middleware): add JsonBodyParser for PSR-7 body stream handling Add JsonBodyParser middleware that solves the php://input non-seekable stream issue by reading the request body once and caching the parsed JSON in the request's parsedBody property. This implements the standard PSR-7 pattern for handling request bodies, allowing controllers to use getParsedBody() instead of reading the stream directly. Usage: $mapper->connect('route', '/path', [ 'controller' => MyController::class, 'stack' => [JsonBodyParser::class], ]); In controllers: $body = $request->getParsedBody(); // array|null Refs: PSR-7 ServerRequestInterface::getParsedBody() --- src/Middleware/JsonBodyParser.php | 77 +++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/Middleware/JsonBodyParser.php diff --git a/src/Middleware/JsonBodyParser.php b/src/Middleware/JsonBodyParser.php new file mode 100644 index 0000000..908f838 --- /dev/null +++ b/src/Middleware/JsonBodyParser.php @@ -0,0 +1,77 @@ +connect('api_endpoint', '/api/endpoint', [ + * 'controller' => MyApiController::class, + * 'stack' => [JsonBodyParser::class], + * ]); + * ``` + * + * In controllers, use getParsedBody() instead of reading getBody(): + * ```php + * public function handle(ServerRequestInterface $request): ResponseInterface + * { + * $body = $request->getParsedBody(); // array|null + * $username = $body['username'] ?? null; + * } + * ``` + * + * Copyright 2026 The Horde Project (http://www.horde.org/) + * + * See the enclosed file LICENSE for license information (LGPL). If you + * did not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @category Horde + * @package Http_Server + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class JsonBodyParser implements MiddlewareInterface +{ + /** + * Process the request and parse JSON body if present + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + // Only parse if Content-Type indicates JSON + $contentType = $request->getHeaderLine('Content-Type'); + + if (str_contains($contentType, 'application/json')) { + // Read body stream once - php://input is non-seekable + $body = (string) $request->getBody(); + + if ($body !== '') { + $parsed = json_decode($body, true); + + // Only set parsedBody if JSON is valid + if (json_last_error() === JSON_ERROR_NONE && is_array($parsed)) { + $request = $request->withParsedBody($parsed); + } + } + } + + return $handler->handle($request); + } +}