mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(api): use HttpError base class for typed-error status mapping (#4746)
* improvement(api): use HttpError base class for typed-error status mapping * chore(api): drop unreachable runtime check after HttpError instanceof guard
This commit is contained in:
@@ -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 {
|
||||
@@ -30,7 +31,7 @@ export interface BlockReferenceResult {
|
||||
blockId: string
|
||||
}
|
||||
|
||||
export class InvalidFieldError extends Error {
|
||||
export class InvalidFieldError extends HttpError {
|
||||
readonly statusCode = 400
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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
|
||||
return status
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user