From 049c6cad2ce00d6e95d1eeea5134e280221ed427 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 4 May 2026 00:00:00 -0700 Subject: [PATCH 1/2] fix(table): return 400 instead of 500 for malformed sort/filter input --- .../sim/app/api/table/[tableId]/rows/route.ts | 14 ++++++++++- .../app/api/v1/tables/[tableId]/rows/route.ts | 14 ++++++++++- apps/sim/lib/table/sql.ts | 23 +++++++++++++++---- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/api/table/[tableId]/rows/route.ts b/apps/sim/app/api/table/[tableId]/rows/route.ts index 8c69ef55a38..9b0076a127d 100644 --- a/apps/sim/app/api/table/[tableId]/rows/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/route.ts @@ -30,7 +30,7 @@ import { validateRowData, validateRowSize, } from '@/lib/table' -import { buildFilterClause, buildSortClause } from '@/lib/table/sql' +import { buildFilterClause, buildSortClause, TableQueryValidationError } from '@/lib/table/sql' import { accessError, checkAccess } from '@/app/api/table/utils' const logger = createLogger('TableRowsAPI') @@ -336,6 +336,10 @@ export const GET = withRouteHandler( return validationErrorResponse(error) } + if (error instanceof TableQueryValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + logger.error(`[${requestId}] Error querying rows:`, error) return NextResponse.json({ error: 'Failed to query rows' }, { status: 500 }) } @@ -421,6 +425,10 @@ export const PUT = withRouteHandler( return validationErrorResponse(error) } + if (error instanceof TableQueryValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + const errorMessage = toError(error).message if ( @@ -520,6 +528,10 @@ export const DELETE = withRouteHandler( return validationErrorResponse(error) } + if (error instanceof TableQueryValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + const errorMessage = toError(error).message if (errorMessage.includes('Filter is required')) { diff --git a/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts b/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts index a6bb5613cad..d4d9c448837 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts @@ -30,7 +30,7 @@ import { validateRowData, validateRowSize, } from '@/lib/table' -import { buildFilterClause, buildSortClause } from '@/lib/table/sql' +import { buildFilterClause, buildSortClause, TableQueryValidationError } from '@/lib/table/sql' import { accessError, checkAccess } from '@/app/api/table/utils' import { checkRateLimit, @@ -240,6 +240,10 @@ export const GET = withRouteHandler(async (request: NextRequest, context: TableR const validationResponse = validationErrorResponseFromError(error) if (validationResponse) return validationResponse + if (error instanceof TableQueryValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + logger.error(`[${requestId}] Error querying rows:`, error) return NextResponse.json({ error: 'Failed to query rows' }, { status: 500 }) } @@ -407,6 +411,10 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: TableR const validationResponse = validationErrorResponseFromError(error) if (validationResponse) return validationResponse + if (error instanceof TableQueryValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + const errorMessage = toError(error).message if ( @@ -500,6 +508,10 @@ export const DELETE = withRouteHandler( const validationResponse = validationErrorResponseFromError(error) if (validationResponse) return validationResponse + if (error instanceof TableQueryValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + const errorMessage = toError(error).message if (errorMessage.includes('Filter is required')) { diff --git a/apps/sim/lib/table/sql.ts b/apps/sim/lib/table/sql.ts index d2004175f44..007e7b8ebf1 100644 --- a/apps/sim/lib/table/sql.ts +++ b/apps/sim/lib/table/sql.ts @@ -10,6 +10,17 @@ import { sql } from 'drizzle-orm' import { NAME_PATTERN } from './constants' import type { ColumnDefinition, ConditionOperators, Filter, JsonValue, Sort } from './types' +/** + * Error thrown when caller-supplied filter or sort input is malformed. + * Routes should map this to HTTP 400 with the message preserved. + */ +export class TableQueryValidationError extends Error { + constructor(message: string) { + super(message) + this.name = 'TableQueryValidationError' + } +} + /** * Whitelist of allowed operators for query filtering. * Only these operators can be used in filter conditions. @@ -133,7 +144,9 @@ export function buildSortClause( validateFieldName(field) if (direction !== 'asc' && direction !== 'desc') { - throw new Error(`Invalid sort direction "${direction}". Must be "asc" or "desc".`) + throw new TableQueryValidationError( + `Invalid sort direction "${direction}". Must be "asc" or "desc".` + ) } const columnType = columnTypeMap.get(field) @@ -152,11 +165,11 @@ export function buildSortClause( */ function validateFieldName(field: string): void { if (!field || typeof field !== 'string') { - throw new Error('Field name must be a non-empty string') + throw new TableQueryValidationError('Field name must be a non-empty string') } if (!NAME_PATTERN.test(field)) { - throw new Error( + throw new TableQueryValidationError( `Invalid field name "${field}". Field names must start with a letter or underscore, followed by alphanumeric characters or underscores.` ) } @@ -170,7 +183,7 @@ function validateFieldName(field: string): void { */ function validateOperator(operator: string): void { if (!ALLOWED_OPERATORS.has(operator)) { - throw new Error( + throw new TableQueryValidationError( `Invalid operator "${operator}". Allowed operators: ${Array.from(ALLOWED_OPERATORS).join(', ')}` ) } @@ -261,7 +274,7 @@ function buildFieldCondition( default: // This should never happen due to validateOperator, but added for completeness - throw new Error(`Unsupported operator: ${op}`) + throw new TableQueryValidationError(`Unsupported operator: ${op}`) } } } else { From 4306ac7c8f232b854f95ae1d068b72040cd746c4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 4 May 2026 00:05:31 -0700 Subject: [PATCH 2/2] fix(table): revert default-case throw to plain Error and update JSDoc @throws tags --- apps/sim/lib/table/sql.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/sim/lib/table/sql.ts b/apps/sim/lib/table/sql.ts index 007e7b8ebf1..f854d2b5237 100644 --- a/apps/sim/lib/table/sql.ts +++ b/apps/sim/lib/table/sql.ts @@ -52,7 +52,7 @@ const ALLOWED_OPERATORS = new Set([ * @param filter - Filter object with field conditions and logical operators * @param tableName - Table name for the query (e.g., 'user_table_rows') * @returns SQL WHERE clause or undefined if no filter specified - * @throws Error if field name is invalid or operator is not allowed + * @throws {TableQueryValidationError} if field name is invalid or operator is not allowed * * @example * // Simple equality @@ -121,7 +121,7 @@ export function buildFilterClause(filter: Filter, tableName: string): SQL | unde * @param tableName - Table name for the query (e.g., 'user_table_rows') * @param columns - Optional column definitions for type-aware sorting * @returns SQL ORDER BY clause or undefined if no sort specified - * @throws Error if field name is invalid + * @throws {TableQueryValidationError} if field name or sort direction is invalid * * @example * buildSortClause({ name: 'asc', age: 'desc' }, 'user_table_rows') @@ -161,7 +161,7 @@ export function buildSortClause( * Field names must match the NAME_PATTERN (alphanumeric + underscore, starting with letter/underscore). * * @param field - The field name to validate - * @throws Error if field name is invalid + * @throws {TableQueryValidationError} if field name is invalid */ function validateFieldName(field: string): void { if (!field || typeof field !== 'string') { @@ -179,7 +179,7 @@ function validateFieldName(field: string): void { * Validates an operator to ensure it's in the allowed list. * * @param operator - The operator to validate - * @throws Error if operator is not allowed + * @throws {TableQueryValidationError} if operator is not allowed */ function validateOperator(operator: string): void { if (!ALLOWED_OPERATORS.has(operator)) { @@ -203,7 +203,7 @@ function validateOperator(operator: string): void { * object with operators like $eq, $gt, $in, etc. * @returns Array of SQL condition fragments. Multiple conditions are returned * when the condition object contains multiple operators. - * @throws Error if field name is invalid or operator is not allowed + * @throws {TableQueryValidationError} if field name is invalid or operator is not allowed */ function buildFieldCondition( tableName: string, @@ -273,8 +273,10 @@ function buildFieldCondition( break default: - // This should never happen due to validateOperator, but added for completeness - throw new TableQueryValidationError(`Unsupported operator: ${op}`) + // This should never happen due to validateOperator, but added for completeness. + // Throw a plain Error (→ 500) since reaching this default means the switch + // and ALLOWED_OPERATORS have drifted — that's a programmer error, not a caller error. + throw new Error(`Unsupported operator: ${op}`) } } } else {