Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/http/StatelessApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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();
}
Expand Down
30 changes: 30 additions & 0 deletions tests/http/ErrorHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
77 changes: 76 additions & 1 deletion tests/http/StatelessApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading