Skip to content

Commit 7e17092

Browse files
committed
fix(api): classify access-denied and sandbox user-code errors with correct HTTP status
1 parent d62f9ca commit 7e17092

11 files changed

Lines changed: 115 additions & 12 deletions

File tree

apps/sim/app/api/copilot/chat/queries.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ import { normalizeMessage } from '@/lib/copilot/chat/persisted-message'
1212
import {
1313
authenticateCopilotRequestSessionOnly,
1414
createBadRequestResponse,
15+
createForbiddenResponse,
1516
createInternalServerErrorResponse,
1617
createUnauthorizedResponse,
1718
} from '@/lib/copilot/request/http'
1819
import { readFilePreviewSessions } from '@/lib/copilot/request/session'
1920
import { readEvents } from '@/lib/copilot/request/session/buffer'
2021
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
21-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
22+
import {
23+
assertActiveWorkspaceAccess,
24+
isWorkspaceAccessDeniedError,
25+
} from '@/lib/workspaces/permissions/utils'
2226

2327
const logger = createLogger('CopilotChatAPI')
2428

@@ -196,6 +200,9 @@ export async function GET(req: NextRequest) {
196200
chats: chats.map(transformChatListItem),
197201
})
198202
} catch (error) {
203+
if (isWorkspaceAccessDeniedError(error)) {
204+
return createForbiddenResponse('Workspace access denied')
205+
}
199206
logger.error('Error fetching copilot chats:', error)
200207
return createInternalServerErrorResponse('Failed to fetch chats')
201208
}

apps/sim/app/api/copilot/chats/route.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import { resolveOrCreateChat } from '@/lib/copilot/chat/lifecycle'
1010
import {
1111
authenticateCopilotRequestSessionOnly,
1212
createBadRequestResponse,
13+
createForbiddenResponse,
1314
createInternalServerErrorResponse,
1415
createUnauthorizedResponse,
1516
} from '@/lib/copilot/request/http'
1617
import { taskPubSub } from '@/lib/copilot/tasks'
1718
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
18-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
19+
import {
20+
assertActiveWorkspaceAccess,
21+
isWorkspaceAccessDeniedError,
22+
} from '@/lib/workspaces/permissions/utils'
1923

2024
const logger = createLogger('CopilotChatsListAPI')
2125

@@ -138,6 +142,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
138142

139143
return NextResponse.json({ success: true, id: result.chatId })
140144
} catch (error) {
145+
if (isWorkspaceAccessDeniedError(error)) {
146+
return createForbiddenResponse('Workspace access denied')
147+
}
141148
logger.error('Error creating workflow copilot chat:', error)
142149
return createInternalServerErrorResponse('Failed to create chat')
143150
}

apps/sim/app/api/function/execute/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,7 +1132,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
11321132
output: { result: null, stdout: cleanStdout(shellStdout), executionTime },
11331133
},
11341134
routeContext,
1135-
{ status: 500 }
1135+
{ status: 422 }
11361136
)
11371137
}
11381138

@@ -1269,7 +1269,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
12691269
output: { result: null, stdout: cleanedOutput, executionTime },
12701270
},
12711271
routeContext,
1272-
{ status: 500 }
1272+
{ status: 422 }
12731273
)
12741274
}
12751275

@@ -1356,7 +1356,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
13561356
output: { result: null, stdout: cleanedOutput, executionTime },
13571357
},
13581358
routeContext,
1359-
{ status: 500 }
1359+
{ status: 422 }
13601360
)
13611361
}
13621362

apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { fetchGo } from '@/lib/copilot/request/go/fetch'
1111
import {
1212
authenticateCopilotRequestSessionOnly,
1313
createBadRequestResponse,
14+
createForbiddenResponse,
1415
createInternalServerErrorResponse,
1516
createNotFoundResponse,
1617
createUnauthorizedResponse,
@@ -21,7 +22,10 @@ import { taskPubSub } from '@/lib/copilot/tasks'
2122
import { env } from '@/lib/core/config/env'
2223
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
2324
import { captureServerEvent } from '@/lib/posthog/server'
24-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
25+
import {
26+
assertActiveWorkspaceAccess,
27+
isWorkspaceAccessDeniedError,
28+
} from '@/lib/workspaces/permissions/utils'
2529

2630
const logger = createLogger('ForkChatAPI')
2731

@@ -150,6 +154,9 @@ export const POST = withRouteHandler(
150154

151155
return NextResponse.json({ success: true, id: newId })
152156
} catch (error) {
157+
if (isWorkspaceAccessDeniedError(error)) {
158+
return createForbiddenResponse('Workspace access denied')
159+
}
153160
logger.error('Error forking chat:', error)
154161
return createInternalServerErrorResponse('Failed to fork chat')
155162
}

apps/sim/app/api/mothership/chats/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ import { parseRequest } from '@/lib/api/server'
1111
import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness'
1212
import {
1313
authenticateCopilotRequestSessionOnly,
14+
createForbiddenResponse,
1415
createInternalServerErrorResponse,
1516
createUnauthorizedResponse,
1617
} from '@/lib/copilot/request/http'
1718
import { taskPubSub } from '@/lib/copilot/tasks'
1819
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1920
import { captureServerEvent } from '@/lib/posthog/server'
20-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
21+
import {
22+
assertActiveWorkspaceAccess,
23+
isWorkspaceAccessDeniedError,
24+
} from '@/lib/workspaces/permissions/utils'
2125

2226
const logger = createLogger('MothershipChatsAPI')
2327

@@ -68,6 +72,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
6872

6973
return NextResponse.json({ success: true, data: reconciled })
7074
} catch (error) {
75+
if (isWorkspaceAccessDeniedError(error)) {
76+
return createForbiddenResponse('Workspace access denied')
77+
}
7178
logger.error('Error fetching mothership chats:', error)
7279
return createInternalServerErrorResponse('Failed to fetch chats')
7380
}
@@ -118,6 +125,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
118125

119126
return NextResponse.json({ success: true, id: chat.id })
120127
} catch (error) {
128+
if (isWorkspaceAccessDeniedError(error)) {
129+
return createForbiddenResponse('Workspace access denied')
130+
}
121131
logger.error('Error creating mothership chat:', error)
122132
return createInternalServerErrorResponse('Failed to create chat')
123133
}

apps/sim/app/api/mothership/execute/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { buildMothershipToolsForRequest } from '@/lib/mothership/settings/runtim
1919
import {
2020
assertActiveWorkspaceAccess,
2121
getUserEntityPermissions,
22+
isWorkspaceAccessDeniedError,
2223
} from '@/lib/workspaces/permissions/utils'
2324

2425
export const maxDuration = 3600
@@ -378,6 +379,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
378379
return NextResponse.json({ error: 'Mothership execution aborted' }, { status: 499 })
379380
}
380381

382+
if (isWorkspaceAccessDeniedError(error)) {
383+
return NextResponse.json({ error: 'Workspace access denied' }, { status: 403 })
384+
}
385+
381386
logger.error(
382387
messageId ? `Mothership execute error [messageId:${messageId}]` : 'Mothership execute error',
383388
{

apps/sim/app/api/tools/file/manage/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
2020
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
2121
import { performMoveWorkspaceFileItems } from '@/lib/workspace-files/orchestration'
22-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
22+
import {
23+
assertActiveWorkspaceAccess,
24+
isWorkspaceAccessDeniedError,
25+
} from '@/lib/workspaces/permissions/utils'
2326

2427
export const dynamic = 'force-dynamic'
2528

@@ -352,6 +355,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
352355
}
353356
}
354357
} catch (error) {
358+
if (isWorkspaceAccessDeniedError(error)) {
359+
return NextResponse.json(
360+
{ success: false, error: 'Workspace access denied' },
361+
{ status: 403 }
362+
)
363+
}
355364
const message = getErrorMessage(error, 'Unknown error')
356365
logger.error('File operation failed', { operation: body.operation, error: message })
357366
return NextResponse.json({ success: false, error: message }, { status: 500 })

apps/sim/lib/copilot/chat/post.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ import { taskPubSub } from '@/lib/copilot/tasks'
4444
import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context'
4545
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
4646
import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils'
47-
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
47+
import {
48+
getUserEntityPermissions,
49+
isWorkspaceAccessDeniedError,
50+
} from '@/lib/workspaces/permissions/utils'
4851
import type { ChatContext } from '@/stores/panel'
4952

5053
export const maxDuration = 3600
@@ -1039,6 +1042,10 @@ export async function handleUnifiedChatPost(req: NextRequest) {
10391042
return validationErrorResponse(error, 'Invalid request data')
10401043
}
10411044

1045+
if (isWorkspaceAccessDeniedError(error)) {
1046+
return NextResponse.json({ error: 'Workspace access denied' }, { status: 403 })
1047+
}
1048+
10421049
logger.error(`[${requestId}] Error handling unified chat request`, {
10431050
error: getErrorMessage(error, 'Unknown error'),
10441051
stack: error instanceof Error ? error.stack : undefined,

apps/sim/lib/copilot/request/http.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export function createBadRequestResponse(message: string): NextResponse {
3535
return NextResponse.json({ error: message }, { status: 400 })
3636
}
3737

38+
export function createForbiddenResponse(message: string): NextResponse {
39+
return NextResponse.json({ error: message }, { status: 403 })
40+
}
41+
3842
export function createNotFoundResponse(message: string): NextResponse {
3943
return NextResponse.json({ error: message }, { status: 404 })
4044
}

apps/sim/lib/core/utils/with-route-handler.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ type RouteHandler<T = unknown> = (
1111
context: T
1212
) => Promise<NextResponse | Response> | NextResponse | Response
1313

14+
/**
15+
* Reads a numeric `statusCode` (4xx or 5xx) off an Error so typed domain errors
16+
* (e.g. `WorkspaceAccessDeniedError`) can map to the correct HTTP status when
17+
* they bubble up unhandled instead of defaulting to 500.
18+
*/
19+
function readTypedErrorStatus(error: unknown): number | undefined {
20+
if (!(error instanceof Error)) return undefined
21+
const status = (error as { statusCode?: unknown }).statusCode
22+
if (typeof status !== 'number') return undefined
23+
if (status < 400 || status >= 600) return undefined
24+
return status
25+
}
26+
1427
/**
1528
* Wraps a Next.js API route handler with centralized error reporting.
1629
*
@@ -35,8 +48,21 @@ export function withRouteHandler<T>(handler: RouteHandler<T>): RouteHandler<T> {
3548
} catch (error) {
3649
const duration = Date.now() - startTime
3750
const message = getErrorMessage(error, 'Unknown error')
38-
logger.error('Unhandled route error', { duration, error: message })
39-
response = NextResponse.json({ error: 'Internal server error', requestId }, { status: 500 })
51+
const typedStatus = readTypedErrorStatus(error)
52+
if (typedStatus !== undefined) {
53+
if (typedStatus >= 500) {
54+
logger.error('Unhandled route error', { duration, status: typedStatus, error: message })
55+
} else {
56+
logger.warn('Typed route error', { duration, status: typedStatus, error: message })
57+
}
58+
response = NextResponse.json({ error: message, requestId }, { status: typedStatus })
59+
} else {
60+
logger.error('Unhandled route error', { duration, error: message })
61+
response = NextResponse.json(
62+
{ error: 'Internal server error', requestId },
63+
{ status: 500 }
64+
)
65+
}
4066
response?.headers?.set('x-request-id', requestId)
4167
return response
4268
}

0 commit comments

Comments
 (0)