Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/sim/executor/utils/block-reference.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HttpError } from '@/lib/core/utils/http-error'
import { USER_FILE_ACCESSIBLE_PROPERTIES } from '@/lib/workflows/types'
import { normalizeName } from '@/executor/constants'
import {
Expand Down Expand Up @@ -30,7 +31,7 @@ export interface BlockReferenceResult {
blockId: string
}

export class InvalidFieldError extends Error {
export class InvalidFieldError extends HttpError {
readonly statusCode = 400

constructor(
Expand Down
19 changes: 19 additions & 0 deletions apps/sim/lib/core/utils/http-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Base class for domain errors that map to a specific HTTP status when they
* bubble up unhandled through `withRouteHandler`. Modeled after NestJS
* `HttpException` / Spring `ResponseStatusException`: subclasses declare a
* concrete `statusCode`, and the centralized route wrapper uses an
* `instanceof HttpError` check (not duck-typing on a `statusCode` property)
* to decide whether to forward the error's `message` to the client.
*
* Using a class check prevents third-party errors that happen to carry a
* `statusCode`-shaped field from being treated as typed HTTP errors and
* leaking internal details.
*
* Subclasses MUST ensure that `message` is safe to expose to clients — no
* stack traces, secrets, file paths, ORM internals, or upstream provider
* details.
*/
export abstract class HttpError extends Error {
abstract readonly statusCode: number
}
24 changes: 14 additions & 10 deletions apps/sim/lib/core/utils/with-route-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createLogger, runWithRequestContext } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { HttpError } from '@/lib/core/utils/http-error'
import { generateRequestId } from '@/lib/core/utils/request'

const logger = createLogger('RouteHandler')
Expand All @@ -12,21 +13,24 @@ type RouteHandler<T = unknown> = (
) => Promise<NextResponse | Response> | NextResponse | Response

/**
* Reads a numeric `statusCode` (4xx or 5xx) off an Error so typed domain errors
* (e.g. `WorkspaceAccessDeniedError`, `InvalidFieldError`) map to the correct
* HTTP status when they bubble up unhandled instead of defaulting to 500.
* Reads a numeric `statusCode` (4xx or 5xx) off an `HttpError` so typed domain
* errors (e.g. `WorkspaceAccessDeniedError`, `InvalidFieldError`) map to the
* correct HTTP status when they bubble up unhandled instead of defaulting to
* 500.
*
* Uses an `instanceof HttpError` check (not duck-typing on `statusCode`) so
* third-party errors that happen to carry a `statusCode`-shaped field cannot
* trigger this path and leak their internal `message` to the client.
*
* When a typed status is returned, the error's `message` is sent to the client
* verbatim — matching the NestJS `HttpException` / Spring `ResponseStatusException`
* convention. The safety contract is convention-based: only attach `statusCode`
* to errors whose `message` is safe to expose to clients (no stack traces,
* secrets, file paths, ORM internals). Untyped errors fall back to a generic
* 500 response with no message exposure.
* convention. Subclasses of `HttpError` are responsible for keeping `message`
* safe to expose to clients (no stack traces, secrets, file paths, ORM
* internals).
*/
function readTypedErrorStatus(error: unknown): number | undefined {
if (!(error instanceof Error)) return undefined
const status = (error as { statusCode?: unknown }).statusCode
if (typeof status !== 'number') return undefined
if (!(error instanceof HttpError)) return undefined
const status = error.statusCode
if (status < 400 || status >= 600) return undefined
Comment thread
waleedlatif1 marked this conversation as resolved.
return status
}
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/lib/workspaces/permissions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
workspace,
} from '@sim/db/schema'
import { and, eq, isNull } from 'drizzle-orm'
import { HttpError } from '@/lib/core/utils/http-error'

export type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
export interface WorkspaceBasic {
Expand Down Expand Up @@ -153,7 +154,7 @@ export async function checkWorkspaceAccess(
* centralized route wrapper maps it to HTTP 403 instead of defaulting to 500.
* The `message` is intentionally client-safe and is exposed to API responses.
*/
export class WorkspaceAccessDeniedError extends Error {
export class WorkspaceAccessDeniedError extends HttpError {
readonly statusCode = 403
readonly workspaceId: string

Expand Down
Loading