Skip to content

Commit 00c424b

Browse files
improvement(executor): reserved keyword errors (#4482)
* improvment(executor): reserved keyword errors * address comments and make error messages for func execute make sense block ref accs
1 parent 690b7ab commit 00c424b

10 files changed

Lines changed: 450 additions & 51 deletions

File tree

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,36 @@ describe('Function Execute API Route', () => {
538538
expect(data.error).toContain('undefinedVariable is not defined')
539539
})
540540

541+
it('should show original source code when resolved block references cause syntax errors', async () => {
542+
mockExecuteInIsolatedVM.mockResolvedValueOnce({
543+
result: null,
544+
stdout: '',
545+
error: {
546+
message: 'Unexpected identifier "globalThis"',
547+
name: 'SyntaxError',
548+
line: 1,
549+
column: 7,
550+
lineContent: 'retur globalThis["__blockRef_0"]',
551+
},
552+
})
553+
554+
const req = createMockRequest('POST', {
555+
code: 'retur globalThis["__blockRef_0"]',
556+
sourceCode: 'retur <start.reqerror>',
557+
contextVariables: { __blockRef_0: 'value' },
558+
timeout: 5000,
559+
})
560+
561+
const response = await POST(req)
562+
const data = await response.json()
563+
564+
expect(response.status).toBe(422)
565+
expect(data.success).toBe(false)
566+
expect(data.error).toContain('Line 1: `retur <start.reqerror>`')
567+
expect(data.error).not.toContain('globalThis')
568+
expect(data.debug.lineContent).toBe('retur <start.reqerror>')
569+
})
570+
541571
it('should handle thrown errors gracefully', async () => {
542572
const req = createMockRequest('POST', {
543573
code: 'throw new Error("Custom error message");',

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

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,30 @@ function createUserFriendlyErrorMessage(
410410
return errorMessage
411411
}
412412

413+
function getErrorDisplayCode(sourceCode: string | undefined, resolvedCode: string): string {
414+
return sourceCode && sourceCode.length > 0 ? sourceCode : resolvedCode
415+
}
416+
417+
function getLineContent(code: string, line: number | undefined): string | undefined {
418+
if (line === undefined || line < 1) {
419+
return undefined
420+
}
421+
422+
return code.split('\n')[line - 1]?.trim()
423+
}
424+
425+
function getErrorDisplayMessage(
426+
message: string,
427+
sourceCode: string | undefined,
428+
resolvedCode: string
429+
): string {
430+
if (!sourceCode || sourceCode === resolvedCode || !resolvedCode.includes('__blockRef_')) {
431+
return message
432+
}
433+
434+
return message.replace(/\s+["']globalThis["']/g, '')
435+
}
436+
413437
function resolveWorkflowVariables(
414438
code: string,
415439
workflowVariables: Record<string, any>,
@@ -767,6 +791,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
767791
let stdout = ''
768792
let userCodeStartLine = 3 // Default value for error reporting
769793
let resolvedCode = '' // Store resolved code for error reporting
794+
let sourceCodeForErrors: string | undefined
770795

771796
try {
772797
const auth = await checkInternalAuth(req)
@@ -783,6 +808,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
783808

784809
const {
785810
code,
811+
sourceCode,
786812
params = {},
787813
timeout = DEFAULT_EXECUTION_TIMEOUT_MS,
788814
language = DEFAULT_CODE_LANGUAGE,
@@ -801,6 +827,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
801827
isCustomTool = false,
802828
_sandboxFiles,
803829
} = body
830+
sourceCodeForErrors = sourceCode
804831

805832
const executionParams = { ...params }
806833
executionParams._context = undefined
@@ -1019,11 +1046,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
10191046
})
10201047

10211048
if (e2bError) {
1049+
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
10221050
const { formattedError, cleanedOutput } = formatE2BError(
1023-
e2bError,
1051+
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
10241052
e2bStdout,
10251053
lang,
1026-
resolvedCode,
1054+
errorDisplayCode,
10271055
prologueLineCount + importLineCount
10281056
)
10291057
return NextResponse.json(
@@ -1101,11 +1129,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
11011129
})
11021130

11031131
if (e2bError) {
1132+
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
11041133
const { formattedError, cleanedOutput } = formatE2BError(
1105-
e2bError,
1134+
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
11061135
e2bStdout,
11071136
lang,
1108-
resolvedCode,
1137+
errorDisplayCode,
11091138
prologueLineCount
11101139
)
11111140
return NextResponse.json(
@@ -1192,13 +1221,16 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
11921221
let adjustedLineContent = ivmError.lineContent
11931222
if (prependedLineCount > 0 && ivmError.line !== undefined) {
11941223
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
1195-
const codeLines = resolvedCode.split('\n')
1196-
if (adjustedLine <= codeLines.length) {
1197-
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()
1198-
}
11991224
}
1225+
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
1226+
const displayMessage = getErrorDisplayMessage(
1227+
ivmError.message,
1228+
sourceCodeForErrors,
1229+
resolvedCode
1230+
)
1231+
adjustedLineContent = getLineContent(errorDisplayCode, adjustedLine) ?? adjustedLineContent
12001232
const enhancedError: EnhancedError = {
1201-
message: ivmError.message,
1233+
message: displayMessage,
12021234
name: ivmError.name,
12031235
stack: ivmError.stack,
12041236
originalError: ivmError,
@@ -1210,7 +1242,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
12101242
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
12111243
enhancedError,
12121244
requestId,
1213-
resolvedCode
1245+
errorDisplayCode
12141246
)
12151247

12161248
const detailLogFn = isSystemError ? logger.error.bind(logger) : logger.warn.bind(logger)
@@ -1261,11 +1293,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
12611293
executionTime,
12621294
})
12631295

1264-
const enhancedError = extractEnhancedError(error, userCodeStartLine, resolvedCode)
1296+
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
1297+
const enhancedError = extractEnhancedError(error, userCodeStartLine, errorDisplayCode)
12651298
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
12661299
enhancedError,
12671300
requestId,
1268-
resolvedCode
1301+
errorDisplayCode
12691302
)
12701303

12711304
logger.error(`[${requestId}] Enhanced error details`, {

apps/sim/executor/handlers/function/function-handler.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,25 @@ describe('FunctionBlockHandler', () => {
196196
)
197197
})
198198

199+
it('should pass original function code for error display after reference resolution', async () => {
200+
mockBlock.config.params = { code: 'retur <start.reqerror>' }
201+
202+
await handler.execute(mockContext, mockBlock, {
203+
code: 'retur globalThis["__blockRef_0"]',
204+
[FUNCTION_BLOCK_CONTEXT_VARS_KEY]: { __blockRef_0: 'value' },
205+
})
206+
207+
expect(mockExecuteTool).toHaveBeenCalledWith(
208+
'function_execute',
209+
expect.objectContaining({
210+
code: 'retur globalThis["__blockRef_0"]',
211+
sourceCode: 'retur <start.reqerror>',
212+
}),
213+
false,
214+
mockContext
215+
)
216+
})
217+
199218
it('should normalize malformed execution context records before calling function_execute', async () => {
200219
const legacyVariable = { id: 'var-1', name: 'brand', type: 'plain', value: 'myfitness' }
201220
mockContext.workflowVariables = [legacyVariable] as unknown as Record<string, any>

apps/sim/executor/handlers/function/function-handler.ts

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver'
1212
import type { SerializedBlock } from '@/serializer/types'
1313
import { executeTool } from '@/tools'
1414

15+
function readCodeContent(value: unknown): string | undefined {
16+
if (typeof value === 'string') {
17+
return value
18+
}
19+
20+
if (Array.isArray(value)) {
21+
return value
22+
.map((entry) =>
23+
entry && typeof entry === 'object' && typeof entry.content === 'string' ? entry.content : ''
24+
)
25+
.join('\n')
26+
}
27+
28+
return undefined
29+
}
30+
1531
/**
1632
* Handler for Function blocks that execute custom code.
1733
*/
@@ -25,37 +41,36 @@ export class FunctionBlockHandler implements BlockHandler {
2541
block: SerializedBlock,
2642
inputs: Record<string, any>
2743
): Promise<any> {
28-
const codeContent = Array.isArray(inputs.code)
29-
? inputs.code.map((c: { content: string }) => c.content).join('\n')
30-
: inputs.code
44+
const codeContent = readCodeContent(inputs.code) ?? inputs.code
45+
const sourceCode = readCodeContent(
46+
(block.config?.params as Record<string, unknown> | undefined)?.code
47+
)
3148

3249
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
3350

3451
const contextVariables = normalizeRecord(inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY])
3552

36-
const result = await executeTool(
37-
'function_execute',
38-
{
39-
code: codeContent,
40-
language: inputs.language || DEFAULT_CODE_LANGUAGE,
41-
timeout: inputs.timeout || DEFAULT_EXECUTION_TIMEOUT_MS,
42-
envVars: normalizeStringRecord(ctx.environmentVariables),
43-
workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables),
44-
blockData,
45-
blockNameMapping,
46-
blockOutputSchemas,
47-
contextVariables,
48-
_context: {
49-
workflowId: ctx.workflowId,
50-
workspaceId: ctx.workspaceId,
51-
userId: ctx.userId,
52-
isDeployedContext: ctx.isDeployedContext,
53-
enforceCredentialAccess: ctx.enforceCredentialAccess,
54-
},
53+
const toolParams = {
54+
code: codeContent,
55+
...(sourceCode ? { sourceCode } : {}),
56+
language: inputs.language || DEFAULT_CODE_LANGUAGE,
57+
timeout: inputs.timeout || DEFAULT_EXECUTION_TIMEOUT_MS,
58+
envVars: normalizeStringRecord(ctx.environmentVariables),
59+
workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables),
60+
blockData,
61+
blockNameMapping,
62+
blockOutputSchemas,
63+
contextVariables,
64+
_context: {
65+
workflowId: ctx.workflowId,
66+
workspaceId: ctx.workspaceId,
67+
userId: ctx.userId,
68+
isDeployedContext: ctx.isDeployedContext,
69+
enforceCredentialAccess: ctx.enforceCredentialAccess,
5570
},
56-
false,
57-
ctx
58-
)
71+
}
72+
73+
const result = await executeTool('function_execute', toolParams, false, ctx)
5974

6075
if (!result.success) {
6176
throw new Error(result.error || 'Function execution failed')

apps/sim/executor/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,15 @@ export interface NormalizedBlockOutput {
213213
_pauseMetadata?: PauseMetadata
214214
}
215215

216+
export const EXECUTION_CONTROL_OUTPUT_FIELD_NAMES = [
217+
'error',
218+
'selectedOption',
219+
'selectedRoute',
220+
'_pauseMetadata',
221+
] as const
222+
223+
export type ExecutionControlOutputFieldName = (typeof EXECUTION_CONTROL_OUTPUT_FIELD_NAMES)[number]
224+
216225
export interface BlockLog {
217226
blockId: string
218227
blockName?: string

0 commit comments

Comments
 (0)