From b75074be590c38dbd26101c162130dceacc7a2c5 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 23 Apr 2026 16:42:50 -0700 Subject: [PATCH 1/2] Classify ClickHouse NO_COMMON_TYPE (386) as a safe error Admins running SQL via /internal/analytics/query were getting a generic "unknown Clickhouse error" with a Sentry capture when their query hit a type mismatch (e.g. `SELECT [1, 'a']`). 386 is triggered by user-authored SQL, so surface the real message and add a regression test confirming no row data leaks when the same error fires against internal cross-project tables. --- apps/backend/src/lib/clickhouse-errors.ts | 1 + .../endpoints/api/v1/analytics-query.test.ts | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/apps/backend/src/lib/clickhouse-errors.ts b/apps/backend/src/lib/clickhouse-errors.ts index 60e15398ba..d29a9838ef 100644 --- a/apps/backend/src/lib/clickhouse-errors.ts +++ b/apps/backend/src/lib/clickhouse-errors.ts @@ -6,6 +6,7 @@ const SAFE_CLICKHOUSE_ERROR_CODES = [ 159, // TIMEOUT_EXCEEDED 164, // READONLY 158, // TOO_MANY_ROWS + 386, // NO_COMMON_TYPE 396, // TOO_MANY_ROWS_OR_BYTES 636, // CANNOT_EXTRACT_TABLE_STRUCTURE ]; diff --git a/apps/e2e/tests/backend/endpoints/api/v1/analytics-query.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/analytics-query.test.ts index afcdc93243..f8455a8eee 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/analytics-query.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/analytics-query.test.ts @@ -232,6 +232,31 @@ it("handles invalid SQL query", async ({ expect }) => { `); }); +it("does not leak data from the internal cross-project users table via type-mismatch errors", async ({ expect }) => { + const response = await runQuery({ + query: "SELECT if(1, primary_email, 1) AS leaked FROM analytics_internal.users LIMIT 1", + }); + + expect(response.status).toBe(400); + const errorText = JSON.stringify(response.body); + expect(errorText).not.toContain("@"); + expect(errorText).not.toMatch(/primary_email\s*[:=]\s*['"]/); + expect(stripQueryId(response, expect)).toMatchInlineSnapshot(` + NiceResponse { + "status": 400, + "body": { + "code": "ANALYTICS_QUERY_ERROR", + "details": { "error": "There is no supertype for types String, UInt8 because some of them are String\\\\/FixedString\\\\/Enum and some of them are not: In scope SELECT if(1, primary_email, 1) AS leaked FROM analytics_internal.users LIMIT 1. " }, + "error": "There is no supertype for types String, UInt8 because some of them are String\\\\/FixedString\\\\/Enum and some of them are not: In scope SELECT if(1, primary_email, 1) AS leaked FROM analytics_internal.users LIMIT 1. ", + }, + "headers": Headers { + "x-stack-known-error": "ANALYTICS_QUERY_ERROR", +