Skip to content
Merged
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
"yiisoft/yii2": "^2.0.53|^22"
},
"require-dev": {
"infection/infection": "^0.27|^0.31",
"httpsoft/http-message": "^1.1",
"infection/infection": "^0.27|^0.31",
"maglnet/composer-require-checker": "^4.1",
"php-forge/support": "^0.1.0",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan-strict-rules": "^2.0.3",
"phpunit/phpunit": "^10.5",
Expand Down
54 changes: 48 additions & 6 deletions src/http/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@
*/
final class ErrorHandler extends \yii\web\ErrorHandler
{
/**
* Default configuration for creating fallback Response instances.
*
* @phpstan-var array<string, mixed>
*/
public array $defaultResponseConfig = [
'charset' => 'UTF-8',
];

/**
* Template Response instance for error handling.
*
* When set, this Response instance will be used as a base for error responses, preserving configured format,
* formatters, and other settings.
*/
private Response|null $templateResponse = null;

/**
* Clears all output buffers above the minimum required level.
*
Expand Down Expand Up @@ -68,10 +85,10 @@
{
$currentLevel = ob_get_level();

$minLevel = YII_ENV_TEST ? 1 : 0;

Check warning on line 88 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ public function clearOutput(): void { $currentLevel = ob_get_level(); - $minLevel = YII_ENV_TEST ? 1 : 0; + $minLevel = YII_ENV_TEST ? 1 : 1; while ($currentLevel > $minLevel) { if (@ob_end_clean() === false) { // @codeCoverageIgnoreStart

Check warning on line 88 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ public function clearOutput(): void { $currentLevel = ob_get_level(); - $minLevel = YII_ENV_TEST ? 1 : 0; + $minLevel = YII_ENV_TEST ? 1 : -1; while ($currentLevel > $minLevel) { if (@ob_end_clean() === false) { // @codeCoverageIgnoreStart

Check warning on line 88 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ public function clearOutput(): void { $currentLevel = ob_get_level(); - $minLevel = YII_ENV_TEST ? 1 : 0; + $minLevel = YII_ENV_TEST ? 1 : 1; while ($currentLevel > $minLevel) { if (@ob_end_clean() === false) { // @codeCoverageIgnoreStart

Check warning on line 88 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ public function clearOutput(): void { $currentLevel = ob_get_level(); - $minLevel = YII_ENV_TEST ? 1 : 0; + $minLevel = YII_ENV_TEST ? 1 : -1; while ($currentLevel > $minLevel) { if (@ob_end_clean() === false) { // @codeCoverageIgnoreStart

while ($currentLevel > $minLevel) {
if (@ob_end_clean() === false) {

Check warning on line 91 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "Identical": @@ @@ $currentLevel = ob_get_level(); $minLevel = YII_ENV_TEST ? 1 : 0; while ($currentLevel > $minLevel) { - if (@ob_end_clean() === false) { + if (@ob_end_clean() !== false) { // @codeCoverageIgnoreStart ob_clean(); // @codeCoverageIgnoreEnd

Check warning on line 91 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "Identical": @@ @@ $currentLevel = ob_get_level(); $minLevel = YII_ENV_TEST ? 1 : 0; while ($currentLevel > $minLevel) { - if (@ob_end_clean() === false) { + if (@ob_end_clean() !== false) { // @codeCoverageIgnoreStart ob_clean(); // @codeCoverageIgnoreEnd
// @codeCoverageIgnoreStart
ob_clean();
// @codeCoverageIgnoreEnd
Expand Down Expand Up @@ -100,9 +117,9 @@
{
$this->exception = $exception;

$this->unregister();

Check warning on line 120 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ public function handleException($exception): Response { $this->exception = $exception; - $this->unregister(); + if (PHP_SAPI !== 'cli') { $statusCode = 500; if ($exception instanceof HttpException) {

Check warning on line 120 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ public function handleException($exception): Response { $this->exception = $exception; - $this->unregister(); + if (PHP_SAPI !== 'cli') { $statusCode = 500; if ($exception instanceof HttpException) {

if (PHP_SAPI !== 'cli') {

Check warning on line 122 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "NotIdentical": @@ @@ { $this->exception = $exception; $this->unregister(); - if (PHP_SAPI !== 'cli') { + if (PHP_SAPI === 'cli') { $statusCode = 500; if ($exception instanceof HttpException) { $statusCode = $exception->statusCode;

Check warning on line 122 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "NotIdentical": @@ @@ { $this->exception = $exception; $this->unregister(); - if (PHP_SAPI !== 'cli') { + if (PHP_SAPI === 'cli') { $statusCode = 500; if ($exception instanceof HttpException) { $statusCode = $exception->statusCode;
$statusCode = 500;

if ($exception instanceof HttpException) {
Expand All @@ -113,10 +130,10 @@
}

try {
$this->logException($exception);

Check warning on line 133 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ http_response_code($statusCode); } try { - $this->logException($exception); + if ($this->discardExistingOutput) { $this->clearOutput(); }

Check warning on line 133 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ http_response_code($statusCode); } try { - $this->logException($exception); + if ($this->discardExistingOutput) { $this->clearOutput(); }

if ($this->discardExistingOutput) {
$this->clearOutput();

Check warning on line 136 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ try { $this->logException($exception); if ($this->discardExistingOutput) { - $this->clearOutput(); + } $response = $this->renderException($exception); } catch (Throwable $e) {

Check warning on line 136 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ try { $this->logException($exception); if ($this->discardExistingOutput) { - $this->clearOutput(); + } $response = $this->renderException($exception); } catch (Throwable $e) {
}

$response = $this->renderException($exception);
Expand All @@ -129,6 +146,19 @@
return $response;
}

/**
* Sets the template Response for error handling.
*
* The provided Response will be used as a template for error responses, preserving configuration such as format,
* charset, and formatters. The Response will be cleared of any existing data before use.
*
* @param Response $response Template response with desired configuration.
*/
public function setResponse(Response $response): void
{
$this->templateResponse = $response;
}

/**
* Handles fallback exception rendering when an error occurs during exception processing.
*
Expand All @@ -145,23 +175,20 @@
*/
protected function handleFallbackExceptionMessage($exception, $previousException): Response
{
$response = new Response();
$response = $this->createErrorResponse();

$msg = "An Error occurred while handling another error:\n";

$msg .= $exception;

$msg .= "\nPrevious exception:\n";

$msg .= $previousException;

$response->data = 'An internal server error occurred.';

if (YII_DEBUG) {
$response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';

Check warning on line 188 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ $msg .= $previousException; $response->data = 'An internal server error occurred.'; if (YII_DEBUG) { - $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>'; + $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset); $safeServerVars = array_diff_key($_SERVER, array_flip(['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY'])); $response->data .= "\n\$_SERVER = " . VarDumper::export($safeServerVars); }

Check warning on line 188 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ $msg .= $previousException; $response->data = 'An internal server error occurred.'; if (YII_DEBUG) { - $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>'; + $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset); $safeServerVars = array_diff_key($_SERVER, array_flip(['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY'])); $response->data .= "\n\$_SERVER = " . VarDumper::export($safeServerVars); }
$safeServerVars = array_diff_key(

Check warning on line 189 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayDiffKey": @@ @@ $response->data = 'An internal server error occurred.'; if (YII_DEBUG) { $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>'; - $safeServerVars = array_diff_key($_SERVER, array_flip(['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY'])); + $safeServerVars = $_SERVER; $response->data .= "\n\$_SERVER = " . VarDumper::export($safeServerVars); } return $response;

Check warning on line 189 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayDiffKey": @@ @@ $response->data = 'An internal server error occurred.'; if (YII_DEBUG) { $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>'; - $safeServerVars = array_diff_key($_SERVER, array_flip(['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY'])); + $safeServerVars = $_SERVER; $response->data .= "\n\$_SERVER = " . VarDumper::export($safeServerVars); } return $response;
$_SERVER,
array_flip(

Check warning on line 191 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayFlip": @@ @@ $response->data = 'An internal server error occurred.'; if (YII_DEBUG) { $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>'; - $safeServerVars = array_diff_key($_SERVER, array_flip(['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY'])); + $safeServerVars = array_diff_key($_SERVER, ['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY']); $response->data .= "\n\$_SERVER = " . VarDumper::export($safeServerVars); } return $response;

Check warning on line 191 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayFlip": @@ @@ $response->data = 'An internal server error occurred.'; if (YII_DEBUG) { $response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>'; - $safeServerVars = array_diff_key($_SERVER, array_flip(['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY'])); + $safeServerVars = array_diff_key($_SERVER, ['API_KEY', 'AUTH_TOKEN', 'DB_PASSWORD', 'SECRET_KEY']); $response->data .= "\n\$_SERVER = " . VarDumper::export($safeServerVars); } return $response;
[
'API_KEY',
'AUTH_TOKEN',
Expand Down Expand Up @@ -193,7 +220,7 @@
*/
protected function renderException($exception): Response
{
$response = new Response();
$response = $this->createErrorResponse();

$response->setStatusCodeByException($exception);

Expand All @@ -217,7 +244,6 @@
}

$file = $useErrorView ? $this->errorView : $this->exceptionView;

$response->data = $this->renderFile($file, ['exception' => $exception]);
}
} elseif ($response->format === Response::FORMAT_RAW) {
Expand All @@ -228,4 +254,20 @@

return $response;
}

/**
* Creates a Response instance for error handling.
*
* Uses the template Response if available, otherwise creates a new instance with default configuration.
*
* @return Response Clean Response instance ready for error content.
*/
private function createErrorResponse(): Response
{
$response = $this->templateResponse ?? new Response($this->defaultResponseConfig);

$response->clear();

return $response;
}
}
1 change: 1 addition & 0 deletions src/http/StatelessApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ protected function reset(ServerRequestInterface $request): void
$this->requestedAction = null;
$this->requestedParams = [];

$this->errorHandler->setResponse($this->response);
$this->request->setPsr7Request($request);

$this->session->close();
Expand Down
3 changes: 0 additions & 3 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,6 @@ protected function statelessApplication($config = []): StatelessApplication
'scriptFile' => __DIR__ . '/index.php',
'scriptUrl' => '/index.php',
],
'response' => [
'charset' => 'UTF-8',
],
'user' => [
'enableAutoLogin' => false,
'identityClass' => Identity::class,
Expand Down
68 changes: 65 additions & 3 deletions tests/http/StatelessApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace yii2\extensions\psrbridge\tests\http;

use HttpSoft\Message\{ServerRequestFactory, StreamFactory, UploadedFileFactory};
use PHPForge\Support\Assert;
use PHPUnit\Framework\Attributes\{DataProviderExternal, Group, RequiresPhpExtension};
use Psr\Http\Message\{ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface};
use stdClass;
Expand Down Expand Up @@ -429,6 +430,8 @@ public function testContainerResolvesPsrFactoriesWithDefinitions(): void

$container = $app->container();

$app->handle(FactoryHelper::createServerRequestCreator()->createFromGlobals());

self::assertTrue(
$container->has(ServerRequestFactoryInterface::class),
"Container should have definition for 'ServerRequestFactoryInterface', ensuring PSR-7 request factory is " .
Expand Down Expand Up @@ -696,6 +699,8 @@ public function testRecalculateMemoryLimitAfterResetAndIniChange(): void

$app = $this->statelessApplication();

$app->handle(FactoryHelper::createServerRequestCreator()->createFromGlobals());

$firstCalculation = $app->getMemoryLimit();
$app->setMemoryLimit(0);

Expand Down Expand Up @@ -1745,7 +1750,7 @@ public function testUseErrorViewLogicWithDebugFalseAndException(): void
"Response 'content-type' should be 'text/html; charset=UTF-8' for error response when 'Exception' " .
"occurs and 'debug' mode is disabled in 'StatelessApplication'.",
);
self::assertStringContainsString(
Assert::equalsWithoutLE(
<<<HTML
<div id="custom-error-action">
Custom error page from errorAction.
Expand Down Expand Up @@ -1801,7 +1806,7 @@ public function testUseErrorViewLogicWithDebugFalseAndUserException(): void
"Response 'content-type' should be 'text/html; charset=UTF-8' for error response when 'UserException' " .
"occurs and 'debug' mode is disabled in 'StatelessApplication'.",
);
self::assertStringContainsString(
Assert::equalsWithoutLE(
<<<HTML
<div id="custom-error-action">
Custom error page from errorAction.
Expand Down Expand Up @@ -1854,7 +1859,7 @@ public function testUseErrorViewLogicWithDebugTrueAndUserException(): void
"Response 'content-type' should be 'text/html; charset=UTF-8' for error response when 'UserException'" .
"occurs and 'debug' mode is enabled in 'StatelessApplication'.",
);
self::assertStringContainsString(
Assert::equalsWithoutLE(
<<<HTML
<div id="custom-error-action">
Custom error page from errorAction.
Expand All @@ -1872,6 +1877,63 @@ public function testUseErrorViewLogicWithDebugTrueAndUserException(): void
);
}

public function testUseErrorViewLogicWithNonHtmlFormat(): void
{
$_SERVER = [
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => 'site/trigger-exception',
];

$request = FactoryHelper::createServerRequestCreator()->createFromGlobals();

$app = $this->statelessApplication(
[
'components' => [
'errorHandler' => [
'errorAction' => 'site/error',
],
'response' => [
'format' => Response::FORMAT_JSON,
],
],
],
);

$response = $app->handle($request);
$responseBody = $response->getBody()->getContents();

self::assertSame(
500,
$response->getStatusCode(),
"Response 'status code' should be '500' when a 'Exception' occurs with JSON format in " .
"'StatelessApplication', indicating an 'internal server error'.",
);
self::assertSame(
'application/json; charset=UTF-8',
$response->getHeaders()['content-type'][0] ?? '',
"Response 'content-type' should be 'application/json; charset=UTF-8' for error response when 'Exception'" .
"occurs with JSON format in 'StatelessApplication'.",
);
self::assertStringNotContainsString(
'Custom error page from errorAction.',
$responseBody,
"Response 'body' should NOT contain 'Custom error page from errorAction' when format is JSON " .
"because useErrorView should be false regardless of YII_DEBUG or exception type in 'StatelessApplication'.",
);

$decodedResponse = Json::decode($responseBody);

self::assertIsArray(
$decodedResponse,
'JSON response should be decodable to array',
);
self::assertArrayHasKey(
'message',
$decodedResponse,
'JSON error response should contain message key',
);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
Expand Down
2 changes: 0 additions & 2 deletions tests/support/stub/SiteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ public function actionCookie(): void

public function actionError(): string
{
$this->response->format = Response::FORMAT_HTML;

$exception = Yii::$app->errorHandler->exception;

if ($exception !== null) {
Expand Down
Loading