From c725389123f3e8573c4d30ecf2f58cb6cbf2e610 Mon Sep 17 00:00:00 2001 From: jradtilbrook Date: Fri, 8 May 2026 15:27:58 +0800 Subject: [PATCH] Fix false positive error detection when JSON data contains ClickHouse exception text --- src/Statement.php | 10 +++++++++- tests/LargeStreamTest.php | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Statement.php b/src/Statement.php index bc55352..aa7af07 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -111,7 +111,15 @@ private function hasErrorClickhouse(string $body, ?string $contentType): bool { if (strlen($body) > 4096) { $tail = substr($body, -4096); - return preg_match(self::CLICKHOUSE_ERROR_REGEX, $tail) === 1; + if (preg_match(self::CLICKHOUSE_ERROR_REGEX, $tail) === 1) { + // Regex also matches if the actual data contains a ClickHouse error + // string. Use json_validate() to confirm the response is truly broken. + if (function_exists('json_validate')) { + return !json_validate($body); + } + return true; + } + return false; } try { diff --git a/tests/LargeStreamTest.php b/tests/LargeStreamTest.php index 001bdae..3019381 100644 --- a/tests/LargeStreamTest.php +++ b/tests/LargeStreamTest.php @@ -65,6 +65,42 @@ public function testLargeBodyWithErrorAtEnd(): void $this->assertTrue($statement->isError()); } + /** + * Large body with valid JSON containing ClickHouse error text as data. + */ + public function testLargeJsonWithErrorPatternInDataIsNotError(): void + { + if (!function_exists('json_validate')) { + $this->markTestSkipped('json_validate() not available'); + } + + $rows = []; + for ($i = 0; $i < 100; $i++) { + $rows[] = '{"id":' . $i . ',"message":"Code: 60. DB::Exception: Table default.xxx doesn\'t exist. (UNKNOWN_TABLE) (version 24.3.2.23 (official build))"}'; + } + $body = '{"meta":[{"name":"id","type":"UInt64"},{"name":"message","type":"String"}],' + . '"data":[' . implode(',', $rows) . '],' + . '"rows":100,' + . '"statistics":{"elapsed":0.001,"rows_read":100,"bytes_read":4096}}'; + + // Ensure body exceeds the 4096-byte threshold + $this->assertGreaterThan(4096, strlen($body)); + + $responseMock = $this->createMock(CurlerResponse::class); + $responseMock->method('http_code')->willReturn(200); + $responseMock->method('error_no')->willReturn(0); + $responseMock->method('content_type')->willReturn('application/json; charset=UTF-8'); + $responseMock->method('body')->willReturn($body); + + $requestMock = $this->createMock(CurlerRequest::class); + $requestMock->method('response')->willReturn($responseMock); + $requestMock->method('isResponseExists')->willReturn(true); + + $statement = new Statement($requestMock); + + $this->assertFalse($statement->isError()); + } + /** * Small body with valid JSON should still be checked for JSON validity. */