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
30 changes: 30 additions & 0 deletions apps/sim/app/api/function/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,36 @@ describe('Function Execute API Route', () => {
expect(data.error).toContain('undefinedVariable is not defined')
})

it('should show original source code when resolved block references cause syntax errors', async () => {
mockExecuteInIsolatedVM.mockResolvedValueOnce({
result: null,
stdout: '',
error: {
message: 'Unexpected identifier "globalThis"',
name: 'SyntaxError',
line: 1,
column: 7,
lineContent: 'retur globalThis["__blockRef_0"]',
},
})

const req = createMockRequest('POST', {
code: 'retur globalThis["__blockRef_0"]',
sourceCode: 'retur <start.reqerror>',
contextVariables: { __blockRef_0: 'value' },
timeout: 5000,
})

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(422)
expect(data.success).toBe(false)
expect(data.error).toContain('Line 1: `retur <start.reqerror>`')
expect(data.error).not.toContain('globalThis')
expect(data.debug.lineContent).toBe('retur <start.reqerror>')
})

it('should handle thrown errors gracefully', async () => {
const req = createMockRequest('POST', {
code: 'throw new Error("Custom error message");',
Expand Down
57 changes: 45 additions & 12 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,30 @@ function createUserFriendlyErrorMessage(
return errorMessage
}

function getErrorDisplayCode(sourceCode: string | undefined, resolvedCode: string): string {
return sourceCode && sourceCode.length > 0 ? sourceCode : resolvedCode
}

function getLineContent(code: string, line: number | undefined): string | undefined {
if (line === undefined || line < 1) {
return undefined
}

return code.split('\n')[line - 1]?.trim()
}

function getErrorDisplayMessage(
message: string,
sourceCode: string | undefined,
resolvedCode: string
): string {
if (!sourceCode || sourceCode === resolvedCode || !resolvedCode.includes('__blockRef_')) {
return message
}

return message.replace(/\s+["']globalThis["']/g, '')
}

function resolveWorkflowVariables(
code: string,
workflowVariables: Record<string, any>,
Expand Down Expand Up @@ -767,6 +791,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
let stdout = ''
let userCodeStartLine = 3 // Default value for error reporting
let resolvedCode = '' // Store resolved code for error reporting
let sourceCodeForErrors: string | undefined

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

const {
code,
sourceCode,
params = {},
timeout = DEFAULT_EXECUTION_TIMEOUT_MS,
language = DEFAULT_CODE_LANGUAGE,
Expand All @@ -801,6 +827,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
isCustomTool = false,
_sandboxFiles,
} = body
sourceCodeForErrors = sourceCode

const executionParams = { ...params }
executionParams._context = undefined
Expand Down Expand Up @@ -1019,11 +1046,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
})

if (e2bError) {
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
e2bStdout,
lang,
resolvedCode,
errorDisplayCode,
prologueLineCount + importLineCount
)
return NextResponse.json(
Expand Down Expand Up @@ -1101,11 +1129,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
})

if (e2bError) {
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
e2bStdout,
lang,
resolvedCode,
errorDisplayCode,
prologueLineCount
)
return NextResponse.json(
Expand Down Expand Up @@ -1192,13 +1221,16 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
let adjustedLineContent = ivmError.lineContent
if (prependedLineCount > 0 && ivmError.line !== undefined) {
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
const codeLines = resolvedCode.split('\n')
if (adjustedLine <= codeLines.length) {
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()
}
}
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const displayMessage = getErrorDisplayMessage(
ivmError.message,
sourceCodeForErrors,
resolvedCode
)
adjustedLineContent = getLineContent(errorDisplayCode, adjustedLine) ?? adjustedLineContent
const enhancedError: EnhancedError = {
message: ivmError.message,
message: displayMessage,
name: ivmError.name,
stack: ivmError.stack,
originalError: ivmError,
Expand All @@ -1210,7 +1242,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
enhancedError,
requestId,
resolvedCode
errorDisplayCode
)

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

const enhancedError = extractEnhancedError(error, userCodeStartLine, resolvedCode)
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const enhancedError = extractEnhancedError(error, userCodeStartLine, errorDisplayCode)
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
enhancedError,
requestId,
resolvedCode
errorDisplayCode
)

logger.error(`[${requestId}] Enhanced error details`, {
Expand Down
19 changes: 19 additions & 0 deletions apps/sim/executor/handlers/function/function-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,25 @@ describe('FunctionBlockHandler', () => {
)
})

it('should pass original function code for error display after reference resolution', async () => {
mockBlock.config.params = { code: 'retur <start.reqerror>' }

await handler.execute(mockContext, mockBlock, {
code: 'retur globalThis["__blockRef_0"]',
[FUNCTION_BLOCK_CONTEXT_VARS_KEY]: { __blockRef_0: 'value' },
})

expect(mockExecuteTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
code: 'retur globalThis["__blockRef_0"]',
sourceCode: 'retur <start.reqerror>',
}),
false,
mockContext
)
})

it('should normalize malformed execution context records before calling function_execute', async () => {
const legacyVariable = { id: 'var-1', name: 'brand', type: 'plain', value: 'myfitness' }
mockContext.workflowVariables = [legacyVariable] as unknown as Record<string, any>
Expand Down
65 changes: 40 additions & 25 deletions apps/sim/executor/handlers/function/function-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver'
import type { SerializedBlock } from '@/serializer/types'
import { executeTool } from '@/tools'

function readCodeContent(value: unknown): string | undefined {
if (typeof value === 'string') {
return value
}

if (Array.isArray(value)) {
return value
.map((entry) =>
entry && typeof entry === 'object' && typeof entry.content === 'string' ? entry.content : ''
)
.join('\n')
}

return undefined
}

/**
* Handler for Function blocks that execute custom code.
*/
Expand All @@ -25,37 +41,36 @@ export class FunctionBlockHandler implements BlockHandler {
block: SerializedBlock,
inputs: Record<string, any>
): Promise<any> {
const codeContent = Array.isArray(inputs.code)
? inputs.code.map((c: { content: string }) => c.content).join('\n')
: inputs.code
const codeContent = readCodeContent(inputs.code) ?? inputs.code
const sourceCode = readCodeContent(
(block.config?.params as Record<string, unknown> | undefined)?.code
)

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

const contextVariables = normalizeRecord(inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY])

const result = await executeTool(
'function_execute',
{
code: codeContent,
language: inputs.language || DEFAULT_CODE_LANGUAGE,
timeout: inputs.timeout || DEFAULT_EXECUTION_TIMEOUT_MS,
envVars: normalizeStringRecord(ctx.environmentVariables),
workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables),
blockData,
blockNameMapping,
blockOutputSchemas,
contextVariables,
_context: {
workflowId: ctx.workflowId,
workspaceId: ctx.workspaceId,
userId: ctx.userId,
isDeployedContext: ctx.isDeployedContext,
enforceCredentialAccess: ctx.enforceCredentialAccess,
},
const toolParams = {
code: codeContent,
...(sourceCode ? { sourceCode } : {}),
language: inputs.language || DEFAULT_CODE_LANGUAGE,
timeout: inputs.timeout || DEFAULT_EXECUTION_TIMEOUT_MS,
envVars: normalizeStringRecord(ctx.environmentVariables),
workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables),
blockData,
blockNameMapping,
blockOutputSchemas,
contextVariables,
_context: {
workflowId: ctx.workflowId,
workspaceId: ctx.workspaceId,
userId: ctx.userId,
isDeployedContext: ctx.isDeployedContext,
enforceCredentialAccess: ctx.enforceCredentialAccess,
},
false,
ctx
)
}

const result = await executeTool('function_execute', toolParams, false, ctx)

if (!result.success) {
throw new Error(result.error || 'Function execution failed')
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/executor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ export interface NormalizedBlockOutput {
_pauseMetadata?: PauseMetadata
}

export const EXECUTION_CONTROL_OUTPUT_FIELD_NAMES = [
'error',
'selectedOption',
'selectedRoute',
'_pauseMetadata',
] as const

export type ExecutionControlOutputFieldName = (typeof EXECUTION_CONTROL_OUTPUT_FIELD_NAMES)[number]

export interface BlockLog {
blockId: string
blockName?: string
Expand Down
Loading
Loading