From ab346517da9ca9cd7e90f8cc1549da2b80869973 Mon Sep 17 00:00:00 2001 From: odan Date: Wed, 26 Aug 2026 18:23:03 +0200 Subject: [PATCH] Fix process state in long-lived runtimes --- Slim/Interfaces/RouteInterface.php | 21 ------ Slim/Middleware/ErrorExceptionMiddleware.php | 10 +-- Slim/Middleware/JsonExceptionMiddleware.php | 5 +- Slim/Middleware/OutputBufferingMiddleware.php | 14 +++- Slim/Routing/Route.php | 38 ---------- .../ErrorExceptionMiddlewareTest.php | 52 ++++++++++++++ .../JsonExceptionMiddlewareTest.php | 28 ++++++++ .../OutputBufferingMiddlewareTest.php | 72 +++++++++++++++++++ tests/Routing/RouteTest.php | 17 ----- 9 files changed, 170 insertions(+), 87 deletions(-) diff --git a/Slim/Interfaces/RouteInterface.php b/Slim/Interfaces/RouteInterface.php index ad3cc29b8..877cb9abb 100644 --- a/Slim/Interfaces/RouteInterface.php +++ b/Slim/Interfaces/RouteInterface.php @@ -55,27 +55,6 @@ public function getMethods(): array; */ public function getRouteGroup(): ?RouteGroup; - /** - * Retrieve a specific route argument. - */ - public function getArgument(string $name, ?string $default = null): ?string; - - /** - * Get route arguments. - * - * @return array - */ - public function getArguments(): array; - - /** - * Set route arguments. - * - * @param array $arguments The arguments. - * - * @return RouteInterface - */ - public function setArguments(array $arguments): RouteInterface; - /** * @return array */ diff --git a/Slim/Middleware/ErrorExceptionMiddleware.php b/Slim/Middleware/ErrorExceptionMiddleware.php index e37542569..75892d29e 100644 --- a/Slim/Middleware/ErrorExceptionMiddleware.php +++ b/Slim/Middleware/ErrorExceptionMiddleware.php @@ -24,7 +24,7 @@ final class ErrorExceptionMiddleware implements MiddlewareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $errorHandler = set_error_handler(function ($code, $message, $file, $line) { + set_error_handler(function ($code, $message, $file, $line) { $level = error_reporting(); if (($level & $code) === 0) { // silent error @@ -35,13 +35,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface }); try { - $response = $handler->handle($request); + return $handler->handle($request); } finally { - if ($errorHandler) { - restore_error_handler(); - } + restore_error_handler(); } - - return $response; } } diff --git a/Slim/Middleware/JsonExceptionMiddleware.php b/Slim/Middleware/JsonExceptionMiddleware.php index 4d5120759..8683cbbb2 100644 --- a/Slim/Middleware/JsonExceptionMiddleware.php +++ b/Slim/Middleware/JsonExceptionMiddleware.php @@ -23,7 +23,10 @@ final class JsonExceptionMiddleware implements MiddlewareInterface private const DEFAULT_TYPE = 'application/json'; - private int $jsonOptions = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR; + private int $jsonOptions = JSON_PRETTY_PRINT + | JSON_UNESCAPED_SLASHES + | JSON_PARTIAL_OUTPUT_ON_ERROR + | JSON_INVALID_UTF8_SUBSTITUTE; public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { diff --git a/Slim/Middleware/OutputBufferingMiddleware.php b/Slim/Middleware/OutputBufferingMiddleware.php index b1f4bc96c..961c94139 100644 --- a/Slim/Middleware/OutputBufferingMiddleware.php +++ b/Slim/Middleware/OutputBufferingMiddleware.php @@ -21,6 +21,7 @@ use function in_array; use function ob_end_clean; use function ob_get_clean; +use function ob_get_level; use function ob_start; final class OutputBufferingMiddleware implements MiddlewareInterface @@ -54,12 +55,19 @@ public function __construct(StreamFactoryInterface $streamFactory, string $style */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + $level = ob_get_level(); + ob_start(); + try { - ob_start(); $response = $handler->handle($request); - $output = ob_get_clean(); + $output = ''; + while (ob_get_level() > $level) { + $output = (string)ob_get_clean() . $output; + } } catch (Throwable $e) { - ob_end_clean(); + while (ob_get_level() > $level) { + ob_end_clean(); + } throw $e; } diff --git a/Slim/Routing/Route.php b/Slim/Routing/Route.php index d71468990..6d6e90480 100644 --- a/Slim/Routing/Route.php +++ b/Slim/Routing/Route.php @@ -11,8 +11,6 @@ use Slim\Interfaces\MiddlewareCollectionInterface; use Slim\Interfaces\RouteInterface; -use function array_key_exists; - final class Route implements RouteInterface, MiddlewareCollectionInterface { use MiddlewareCollectionTrait; @@ -42,13 +40,6 @@ final class Route implements RouteInterface, MiddlewareCollectionInterface */ private ?RouteGroup $group; - /** - * Route parameters - * - * @var array - */ - private array $arguments = []; - /** * @param array $methods * @param string $pattern @@ -112,33 +103,4 @@ public function getRouteGroup(): ?RouteGroup { return $this->group; } - - /** - * {@inheritdoc} - */ - public function getArgument(string $name, ?string $default = null): ?string - { - if (array_key_exists($name, $this->arguments)) { - return $this->arguments[$name]; - } - return $default; - } - - /** - * {@inheritdoc} - */ - public function getArguments(): array - { - return $this->arguments; - } - - /** - * {@inheritdoc} - */ - public function setArguments(array $arguments): RouteInterface - { - $this->arguments = $arguments; - - return $this; - } } diff --git a/tests/Middleware/ErrorExceptionMiddlewareTest.php b/tests/Middleware/ErrorExceptionMiddlewareTest.php index ea13d57f7..33d1c9f7d 100644 --- a/tests/Middleware/ErrorExceptionMiddlewareTest.php +++ b/tests/Middleware/ErrorExceptionMiddlewareTest.php @@ -118,4 +118,56 @@ public function testProcessReturnsResponse(): void $this->assertSame($response, $result); } + + public function testErrorHandlerIsRestoredWhenNoPreviousHandlerExists(): void + { + $previous = set_error_handler(static fn() => false); + restore_error_handler(); + + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $handler = $this->createMock(RequestHandlerInterface::class); + $handler->expects($this->once()) + ->method('handle') + ->willReturn($response); + + $app = AppFactory::create(); + $middleware = $app->getContainer()->get(ErrorExceptionMiddleware::class); + + $middleware->process($request, $handler); + + $current = set_error_handler(static fn() => false); + restore_error_handler(); + + $this->assertSame($previous, $current); + } + + public function testErrorHandlerIsRestoredWhenAPreviousHandlerExists(): void + { + $custom = static fn() => false; + set_error_handler($custom); + + try { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $handler = $this->createMock(RequestHandlerInterface::class); + $handler->expects($this->once()) + ->method('handle') + ->willReturn($response); + + $app = AppFactory::create(); + $middleware = $app->getContainer()->get(ErrorExceptionMiddleware::class); + + $middleware->process($request, $handler); + + $current = set_error_handler(static fn() => false); + restore_error_handler(); + + $this->assertSame($custom, $current); + } finally { + restore_error_handler(); + } + } } diff --git a/tests/Middleware/JsonExceptionMiddlewareTest.php b/tests/Middleware/JsonExceptionMiddlewareTest.php index 39a88df6c..7917ba65c 100644 --- a/tests/Middleware/JsonExceptionMiddlewareTest.php +++ b/tests/Middleware/JsonExceptionMiddlewareTest.php @@ -198,6 +198,34 @@ public function handle(ServerRequestInterface $request): ResponseInterface $this->assertStringContainsString('Test message', (string)$response->getBody()); } + public function testSubstitutesInvalidUtf8(): 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("Invalid \xB1\x31 UTF-8 sequence"); + } + }; + + $response = $middleware->process($request, $handler); + $decoded = json_decode((string)$response->getBody(), true); + + $this->assertSame(500, $response->getStatusCode()); + $this->assertIsArray($decoded); + $this->assertSame('Application Error', $decoded['message']); + $this->assertSame( + "Invalid \u{FFFD}1 UTF-8 sequence", + $decoded['exception'][0]['message'], + ); + } + public function testWithJsonOptionsChangesEncoding(): void { $middleware = (new JsonExceptionMiddleware(new ResponseFactory())) diff --git a/tests/Middleware/OutputBufferingMiddlewareTest.php b/tests/Middleware/OutputBufferingMiddlewareTest.php index 54978e7c5..7a4f8f8d6 100644 --- a/tests/Middleware/OutputBufferingMiddlewareTest.php +++ b/tests/Middleware/OutputBufferingMiddlewareTest.php @@ -23,6 +23,8 @@ use Slim\Tests\Traits\AppTestTrait; use function ob_get_contents; +use function ob_get_level; +use function ob_start; final class OutputBufferingMiddlewareTest extends TestCase { @@ -146,4 +148,74 @@ public function testOutputBufferIsCleanedWhenThrowableIsCaught() $this->assertSame('', ob_get_contents()); } } + + public function testNestedOutputBuffersAreCapturedAndLevelRestored(): void + { + $level = ob_get_level(); + $app = AppFactory::create(); + + $responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class); + $streamFactory = $app->getContainer()->get(StreamFactoryInterface::class); + + $outputBufferingMiddleware = new OutputBufferingMiddleware($streamFactory, OutputBufferingMiddleware::APPEND); + $app->add($outputBufferingMiddleware); + + $middleware = function () use ($responseFactory) { + $response = $responseFactory->createResponse(); + $response->getBody()->write('Body'); + echo 'Outer'; + ob_start(); + echo 'Inner'; + + return $response; + }; + $app->add($middleware); + $app->addRoutingMiddleware(); + + $request = $app->getContainer() + ->get(ServerRequestFactoryInterface::class) + ->createServerRequest('GET', '/'); + + $response = $app->handle($request); + + $this->assertSame('BodyOuterInner', (string)$response->getBody()); + $this->assertSame($level, ob_get_level()); + } + + public function testNestedOutputBuffersAreRestoredWhenThrowableIsCaught(): void + { + $level = ob_get_level(); + $app = AppFactory::create(); + $streamFactory = $app->getContainer()->get(StreamFactoryInterface::class); + + $middleware = function () { + echo 'Outer'; + ob_start(); + echo 'Inner'; + throw new Exception('Oops...'); + }; + + $outputBufferingMiddleware = new OutputBufferingMiddleware($streamFactory, OutputBufferingMiddleware::PREPEND); + + $app->add($outputBufferingMiddleware); + $app->add($middleware); + $app->addRoutingMiddleware(); + + $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) { + return $response; + }); + + $request = $app->getContainer() + ->get(ServerRequestFactoryInterface::class) + ->createServerRequest('GET', '/'); + + try { + $app->handle($request); + $this->fail('Expected exception was not thrown.'); + } catch (Exception $e) { + $this->assertSame('Oops...', $e->getMessage()); + $this->assertSame($level, ob_get_level()); + $this->assertSame('', (string)ob_get_contents()); + } + } } diff --git a/tests/Routing/RouteTest.php b/tests/Routing/RouteTest.php index c8546edef..343650bfa 100644 --- a/tests/Routing/RouteTest.php +++ b/tests/Routing/RouteTest.php @@ -137,23 +137,6 @@ public function testGetMethodsReturnsCorrectMethods(): void $this->assertSame($methods, $route->getMethods()); } - public function testSetArgumentsStoresStringArguments(): void - { - $methods = ['GET']; - $pattern = '/users/{id}'; - $handler = function () { - return 'handler'; - }; - - $route = new Route($methods, $pattern, $handler); - - $arguments = ['id' => '123', 'slug' => 'john-doe']; - $route->setArguments($arguments); - - $this->assertSame($arguments, $route->getArguments()); - $this->assertSame('123', $route->getArgument('id')); - } - private function createMiddleware(): MiddlewareInterface { return new class implements MiddlewareInterface {