diff --git a/src/http/StatelessApplication.php b/src/http/StatelessApplication.php index c53110dd..bf19c0cf 100644 --- a/src/http/StatelessApplication.php +++ b/src/http/StatelessApplication.php @@ -7,7 +7,6 @@ use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use Psr\Http\Server\RequestHandlerInterface; use Throwable; -use Yii; use yii\base\{Event, InvalidConfigException}; use yii\di\{Container, NotInstantiableException}; use yii\web\{Application, UploadedFile}; @@ -49,6 +48,21 @@ */ final class StatelessApplication extends Application implements RequestHandlerInterface { + /** + * Whether to flush the logger during application termination. + * + * When enabled (default), the logger will be flushed during the {@see terminate()} method, ensuring that all log + * messages are persisted immediately after request processing. + * + * This is the recommended behavior for production environments and worker-based applications. + * + * When disabled, log messages will remain in memory and may accumulate across multiple requests. + * + * **Warning**: Disabling logger flushing in production or worker environments may lead to memory leaks and log + * message loss in case of application crashes. Use with caution and ensure proper memory management. + */ + public bool $flushLogger = true; + /** * Version of the StatelessApplication. */ @@ -446,7 +460,9 @@ protected function terminate(Response $response): ResponseInterface UploadedFile::reset(); - Yii::getLogger()->flush(true); + if ($this->flushLogger) { + $this->getLog()->getLogger()->flush(true); + } return $response->getPsr7Response(); } diff --git a/tests/http/ErrorHandlerTest.php b/tests/http/ErrorHandlerTest.php index 9e4f3f65..f5e6e188 100644 --- a/tests/http/ErrorHandlerTest.php +++ b/tests/http/ErrorHandlerTest.php @@ -153,6 +153,36 @@ public function testHandleExceptionResetsState(): void ); } + public function testHandleExceptionSetsExceptionPropertyAndResetsIt(): void + { + $errorHandler = new ErrorHandler(); + + $errorHandler->discardExistingOutput = false; + + $initialException = self::inaccessibleProperty($errorHandler, 'exception'); + + self::assertNull( + $initialException, + "Exception property should be 'null' initially.", + ); + + $exception = new Exception('Test exception for property verification'); + + $response = $errorHandler->handleException($exception); + + $finalException = self::inaccessibleProperty($errorHandler, 'exception'); + + self::assertNull( + $finalException, + "Exception property should be reset to 'null' after handling exception.", + ); + self::assertSame( + 500, + $response->getStatusCode(), + 'Should set correct status code and reset exception property.', + ); + } + public function testHandleExceptionWithComplexMessage(): void { $errorHandler = new ErrorHandler(); diff --git a/tests/http/StatelessApplicationTest.php b/tests/http/StatelessApplicationTest.php index e076b2ec..291323e5 100644 --- a/tests/http/StatelessApplicationTest.php +++ b/tests/http/StatelessApplicationTest.php @@ -13,7 +13,7 @@ use yii\di\NotInstantiableException; use yii\helpers\Json; use yii\i18n\{Formatter, I18N}; -use yii\log\Dispatcher; +use yii\log\{Dispatcher, FileTarget}; use yii\web\{AssetManager, NotFoundHttpException, Session, UrlManager, User, View}; use yii2\extensions\psrbridge\exception\Message; use yii2\extensions\psrbridge\http\{ErrorHandler, Request, Response}; @@ -580,6 +580,81 @@ public function testGetMemoryLimitHandlesUnlimitedMemoryCorrectly(): void ini_set('memory_limit', $originalLimit); } + /** + * @throws InvalidConfigException if the configuration is invalid or incomplete. + */ + public function testLogExceptionIsCalledWhenHandlingException(): void + { + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => 'site/trigger-exception', + ]; + + $request = FactoryHelper::createServerRequestCreator()->createFromGlobals(); + + $app = $this->statelessApplication( + [ + 'flushLogger' => false, + 'components' => [ + 'errorHandler' => [ + 'errorAction' => null, + ], + 'log' => [ + 'traceLevel' => YII_DEBUG ? 1 : 0, + 'targets' => [ + [ + 'class' => FileTarget::class, + 'levels' => [ + 'error', + ], + ], + ], + ], + ], + ], + ); + + $response = $app->handle($request); + + $logMessages = $app->getLog()->getLogger()->messages; + + self::assertSame( + 500, + $response->getStatusCode(), + "Response 'status code' should be '500' when an exception occurs in 'StatelessApplication'.", + ); + self::assertNotEmpty( + $logMessages, + "Logger should contain log messages after handling an exception in 'StatelessApplication'.", + ); + + $exceptionLogFound = false; + $expectedCategory = Exception::class; + + foreach ($logMessages as $logMessage) { + if ( + is_array($logMessage) && + isset($logMessage[0], $logMessage[2]) && + $logMessage[0] instanceof Exception && + $logMessage[2] === $expectedCategory && + str_contains($logMessage[0]->getMessage(), 'Exception error message.') + ) { + $exceptionLogFound = true; + break; + } + } + + self::assertTrue( + $exceptionLogFound, + "Logger should contain an error log entry with category '{$expectedCategory}' and message 'Exception error message.' " . + "when 'logException()' is called during exception handling in 'StatelessApplication'.", + ); + self::assertFalse( + $app->flushLogger, + "Test must keep logger messages in memory to assert on them; 'flushLogger' should be 'false'.", + ); + } + /** * @throws InvalidConfigException if the configuration is invalid or incomplete. */