= 400 && < 600` comparison * coerced strings to int and could land on a real HTTP status * for the wrong reason. The handler now requires `is_int($code)` * before treating it as an HTTP status. */ final class JsonExceptionHandlerTest extends TestCase { public function testStringCodeFallsBackTo500(): void { // PDOException uses a string SQLSTATE for getCode(). A // hand-rolled subclass below mimics the real-world shape: // string `'400'` would loose-compare as 400 and previously // set the response status to 400, skipping the prod // message-suppression. Now it falls back to 500. $exception = new class () extends \RuntimeException { public function __construct() { parent::__construct('SQLSTATE-style error'); $this->code = '400'; // PDOException stores a string here. } }; $handler = $this->makeHandler(isDev: false); $request = (new ServerRequestFactory())->createServerRequest('GET', '/x'); $response = $handler($request, $exception, false, false, false); self::assertSame(500, $response->getStatusCode()); } public function testIntCodeInRangeIsHonored(): void { $exception = new \RuntimeException('forbidden', 403); $handler = $this->makeHandler(isDev: false); $request = (new ServerRequestFactory())->createServerRequest('GET', '/x'); $response = $handler($request, $exception, false, false, false); self::assertSame(403, $response->getStatusCode()); } public function testIntCodeOutOfRangeFallsBackTo500(): void { $exception = new \RuntimeException('not http', 200); $handler = $this->makeHandler(isDev: false); $request = (new ServerRequestFactory())->createServerRequest('GET', '/x'); $response = $handler($request, $exception, false, false, false); self::assertSame(500, $response->getStatusCode()); } public function testCodeOfZeroFallsBackTo500(): void { // Code 0 is the default for `new \Exception('msg')` — we // must not let it accidentally land on a 400-class status. $exception = new \RuntimeException('plain'); $handler = $this->makeHandler(isDev: false); $request = (new ServerRequestFactory())->createServerRequest('GET', '/x'); $response = $handler($request, $exception, false, false, false); self::assertSame(500, $response->getStatusCode()); } public function testSqlstateLikeStringIsNotCoercedIntoStatus(): void { // PDOException for a missing table is `'42S02'`. Loose // coerce to int gives 42 (out of HTTP range, falls back to // 500 even before the F73 fix). But the SEC_REVIEW called // out a `'400'` that would coerce neatly. Both must be // rejected by the int-only gate. $exception = new class () extends \RuntimeException { public function __construct() { parent::__construct('SQL error'); $this->code = '42S02'; } }; $handler = $this->makeHandler(isDev: false); $request = (new ServerRequestFactory())->createServerRequest('GET', '/x'); $response = $handler($request, $exception, false, false, false); self::assertSame(500, $response->getStatusCode()); } private function makeHandler(bool $isDev): JsonExceptionHandler { $twig = Twig::create(__DIR__ . '/../../../resources/views', ['cache' => false]); $logger = new Logger('test'); $logger->pushHandler(new NullHandler()); return new JsonExceptionHandler($twig, new ResponseFactory(), $logger, $isDev); } }