Skip to content

Commit 4c86122

Browse files
authored
fix(table): return 400 instead of 500 for malformed sort/filter input (#4425)
* fix(table): return 400 instead of 500 for malformed sort/filter input * fix(table): revert default-case throw to plain Error and update JSDoc @throws tags
1 parent 0f09310 commit 4c86122

3 files changed

Lines changed: 51 additions & 12 deletions

File tree

apps/sim/app/api/table/[tableId]/rows/route.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
validateRowData,
3131
validateRowSize,
3232
} from '@/lib/table'
33-
import { buildFilterClause, buildSortClause } from '@/lib/table/sql'
33+
import { buildFilterClause, buildSortClause, TableQueryValidationError } from '@/lib/table/sql'
3434
import { accessError, checkAccess } from '@/app/api/table/utils'
3535

3636
const logger = createLogger('TableRowsAPI')
@@ -336,6 +336,10 @@ export const GET = withRouteHandler(
336336
return validationErrorResponse(error)
337337
}
338338

339+
if (error instanceof TableQueryValidationError) {
340+
return NextResponse.json({ error: error.message }, { status: 400 })
341+
}
342+
339343
logger.error(`[${requestId}] Error querying rows:`, error)
340344
return NextResponse.json({ error: 'Failed to query rows' }, { status: 500 })
341345
}
@@ -421,6 +425,10 @@ export const PUT = withRouteHandler(
421425
return validationErrorResponse(error)
422426
}
423427

428+
if (error instanceof TableQueryValidationError) {
429+
return NextResponse.json({ error: error.message }, { status: 400 })
430+
}
431+
424432
const errorMessage = toError(error).message
425433

426434
if (
@@ -520,6 +528,10 @@ export const DELETE = withRouteHandler(
520528
return validationErrorResponse(error)
521529
}
522530

531+
if (error instanceof TableQueryValidationError) {
532+
return NextResponse.json({ error: error.message }, { status: 400 })
533+
}
534+
523535
const errorMessage = toError(error).message
524536

525537
if (errorMessage.includes('Filter is required')) {

apps/sim/app/api/v1/tables/[tableId]/rows/route.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
validateRowData,
3131
validateRowSize,
3232
} from '@/lib/table'
33-
import { buildFilterClause, buildSortClause } from '@/lib/table/sql'
33+
import { buildFilterClause, buildSortClause, TableQueryValidationError } from '@/lib/table/sql'
3434
import { accessError, checkAccess } from '@/app/api/table/utils'
3535
import {
3636
checkRateLimit,
@@ -240,6 +240,10 @@ export const GET = withRouteHandler(async (request: NextRequest, context: TableR
240240
const validationResponse = validationErrorResponseFromError(error)
241241
if (validationResponse) return validationResponse
242242

243+
if (error instanceof TableQueryValidationError) {
244+
return NextResponse.json({ error: error.message }, { status: 400 })
245+
}
246+
243247
logger.error(`[${requestId}] Error querying rows:`, error)
244248
return NextResponse.json({ error: 'Failed to query rows' }, { status: 500 })
245249
}
@@ -407,6 +411,10 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: TableR
407411
const validationResponse = validationErrorResponseFromError(error)
408412
if (validationResponse) return validationResponse
409413

414+
if (error instanceof TableQueryValidationError) {
415+
return NextResponse.json({ error: error.message }, { status: 400 })
416+
}
417+
410418
const errorMessage = toError(error).message
411419

412420
if (
@@ -500,6 +508,10 @@ export const DELETE = withRouteHandler(
500508
const validationResponse = validationErrorResponseFromError(error)
501509
if (validationResponse) return validationResponse
502510

511+
if (error instanceof TableQueryValidationError) {
512+
return NextResponse.json({ error: error.message }, { status: 400 })
513+
}
514+
503515
const errorMessage = toError(error).message
504516

505517
if (errorMessage.includes('Filter is required')) {

apps/sim/lib/table/sql.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ import { sql } from 'drizzle-orm'
1010
import { NAME_PATTERN } from './constants'
1111
import type { ColumnDefinition, ConditionOperators, Filter, JsonValue, Sort } from './types'
1212

13+
/**
14+
* Error thrown when caller-supplied filter or sort input is malformed.
15+
* Routes should map this to HTTP 400 with the message preserved.
16+
*/
17+
export class TableQueryValidationError extends Error {
18+
constructor(message: string) {
19+
super(message)
20+
this.name = 'TableQueryValidationError'
21+
}
22+
}
23+
1324
/**
1425
* Whitelist of allowed operators for query filtering.
1526
* Only these operators can be used in filter conditions.
@@ -41,7 +52,7 @@ const ALLOWED_OPERATORS = new Set([
4152
* @param filter - Filter object with field conditions and logical operators
4253
* @param tableName - Table name for the query (e.g., 'user_table_rows')
4354
* @returns SQL WHERE clause or undefined if no filter specified
44-
* @throws Error if field name is invalid or operator is not allowed
55+
* @throws {TableQueryValidationError} if field name is invalid or operator is not allowed
4556
*
4657
* @example
4758
* // Simple equality
@@ -110,7 +121,7 @@ export function buildFilterClause(filter: Filter, tableName: string): SQL | unde
110121
* @param tableName - Table name for the query (e.g., 'user_table_rows')
111122
* @param columns - Optional column definitions for type-aware sorting
112123
* @returns SQL ORDER BY clause or undefined if no sort specified
113-
* @throws Error if field name is invalid
124+
* @throws {TableQueryValidationError} if field name or sort direction is invalid
114125
*
115126
* @example
116127
* buildSortClause({ name: 'asc', age: 'desc' }, 'user_table_rows')
@@ -133,7 +144,9 @@ export function buildSortClause(
133144
validateFieldName(field)
134145

135146
if (direction !== 'asc' && direction !== 'desc') {
136-
throw new Error(`Invalid sort direction "${direction}". Must be "asc" or "desc".`)
147+
throw new TableQueryValidationError(
148+
`Invalid sort direction "${direction}". Must be "asc" or "desc".`
149+
)
137150
}
138151

139152
const columnType = columnTypeMap.get(field)
@@ -148,15 +161,15 @@ export function buildSortClause(
148161
* Field names must match the NAME_PATTERN (alphanumeric + underscore, starting with letter/underscore).
149162
*
150163
* @param field - The field name to validate
151-
* @throws Error if field name is invalid
164+
* @throws {TableQueryValidationError} if field name is invalid
152165
*/
153166
function validateFieldName(field: string): void {
154167
if (!field || typeof field !== 'string') {
155-
throw new Error('Field name must be a non-empty string')
168+
throw new TableQueryValidationError('Field name must be a non-empty string')
156169
}
157170

158171
if (!NAME_PATTERN.test(field)) {
159-
throw new Error(
172+
throw new TableQueryValidationError(
160173
`Invalid field name "${field}". Field names must start with a letter or underscore, followed by alphanumeric characters or underscores.`
161174
)
162175
}
@@ -166,11 +179,11 @@ function validateFieldName(field: string): void {
166179
* Validates an operator to ensure it's in the allowed list.
167180
*
168181
* @param operator - The operator to validate
169-
* @throws Error if operator is not allowed
182+
* @throws {TableQueryValidationError} if operator is not allowed
170183
*/
171184
function validateOperator(operator: string): void {
172185
if (!ALLOWED_OPERATORS.has(operator)) {
173-
throw new Error(
186+
throw new TableQueryValidationError(
174187
`Invalid operator "${operator}". Allowed operators: ${Array.from(ALLOWED_OPERATORS).join(', ')}`
175188
)
176189
}
@@ -190,7 +203,7 @@ function validateOperator(operator: string): void {
190203
* object with operators like $eq, $gt, $in, etc.
191204
* @returns Array of SQL condition fragments. Multiple conditions are returned
192205
* when the condition object contains multiple operators.
193-
* @throws Error if field name is invalid or operator is not allowed
206+
* @throws {TableQueryValidationError} if field name is invalid or operator is not allowed
194207
*/
195208
function buildFieldCondition(
196209
tableName: string,
@@ -260,7 +273,9 @@ function buildFieldCondition(
260273
break
261274

262275
default:
263-
// This should never happen due to validateOperator, but added for completeness
276+
// This should never happen due to validateOperator, but added for completeness.
277+
// Throw a plain Error (→ 500) since reaching this default means the switch
278+
// and ALLOWED_OPERATORS have drifted — that's a programmer error, not a caller error.
264279
throw new Error(`Unsupported operator: ${op}`)
265280
}
266281
}

0 commit comments

Comments
 (0)