import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { functionExecuteContract } from '@/lib/api/contracts' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { FORMAT_TO_CONTENT_TYPE, getOutputFileDeclarations, normalizeOutputWorkspaceFileName, type OutputFileDeclaration, resolveOutputFormat, } from '@/lib/copilot/request/tools/files' import { validateWorkspaceFileWriteTarget, writeWorkspaceFileByPath, } from '@/lib/copilot/vfs/resource-writer' import { isE2bEnabled } from '@/lib/core/config/env-flags' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { executeInE2B, executeShellInE2B } from '@/lib/execution/e2b' import { executeInIsolatedVM, type IsolatedVMBrokerHandler } from '@/lib/execution/isolated-vm' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { recordMaterializedAccessKeys } from '@/lib/execution/payloads/access-keys' import { isLargeArrayManifest, materializeLargeArrayManifest, } from '@/lib/execution/payloads/large-array-manifest' import { containsLargeValueRef, isLargeValueRef } from '@/lib/execution/payloads/large-value-ref' import { MAX_FUNCTION_INLINE_BYTES, MAX_INLINE_MATERIALIZATION_BYTES, readUserFileContent, unavailableLargeValueError, } from '@/lib/execution/payloads/materialization.server' import { compactExecutionPayload } from '@/lib/execution/payloads/serializer' import { materializeLargeValueRef } from '@/lib/execution/payloads/store' import { isExecutionResourceLimitError } from '@/lib/execution/resource-errors' import { getWorkflowById } from '@/lib/workflows/utils' import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants' import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference' import { formatLiteralForCode } from '@/executor/utils/code-formatting' import { createEnvVarPattern, createReferencePattern, createWorkflowVariablePattern, } from '@/executor/utils/reference-validation' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' const logger = createLogger('FunctionExecuteAPI') const TAG_PATTERN = createReferencePattern() const E2B_JS_WRAPPER_LINES = 3 const E2B_PYTHON_WRAPPER_LINES = 1 const MAX_SANDBOX_OUTPUT_FILES = 20 const MAX_SANDBOX_OUTPUT_BYTES = 50 * 1024 * 1024 /** Matches valid JS identifier names (letters, digits, underscore; no leading digit). */ const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/ /** ES2023 reserved words — using these as `const` variable names produces a SyntaxError. */ const JS_RESERVED_WORDS = new Set([ 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'null', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'enum', 'await', 'implements', 'interface', 'package', 'private', 'protected', 'public', ]) type TypeScriptModule = typeof import('@typescript/typescript6') let typescriptModulePromise: Promise | null = null async function loadTypeScriptModule(): Promise { if (!typescriptModulePromise) { typescriptModulePromise = import('@typescript/typescript6').then( (mod) => (mod?.default ?? mod) as TypeScriptModule, (error) => { typescriptModulePromise = null throw error } ) } return typescriptModulePromise } async function extractJavaScriptImports( code: string ): Promise<{ imports: string; remainingCode: string; importLineCount: number }> { try { const tsModule = await loadTypeScriptModule() const sourceFile = tsModule.createSourceFile( 'user-code.js', code, tsModule.ScriptTarget.Latest, true, tsModule.ScriptKind.JS ) const importSegments: Array<{ text: string; start: number; end: number }> = [] sourceFile.statements.forEach((statement) => { if ( tsModule.isImportDeclaration(statement) || tsModule.isImportEqualsDeclaration(statement) ) { importSegments.push({ text: statement.getFullText(sourceFile).trim(), start: statement.getFullStart(), end: statement.getEnd(), }) } }) if (importSegments.length === 0) { return { imports: '', remainingCode: code, importLineCount: 0 } } importSegments.sort((a, b) => a.start - b.start) const imports = importSegments.map((segment) => segment.text).join('\n') let cursor = 0 const parts: string[] = [] let importLineCount = 0 for (const segment of importSegments) { if (segment.start > cursor) { parts.push(code.slice(cursor, segment.start)) } const removedSegment = code.slice(segment.start, segment.end) importLineCount += removedSegment.split('\n').length - 1 const newlinePlaceholder = removedSegment.replace(/[^\n]/g, '') parts.push(newlinePlaceholder) cursor = segment.end } if (cursor < code.length) { parts.push(code.slice(cursor)) } const remainingCode = parts.join('') return { imports, remainingCode, importLineCount: Math.max(importLineCount, 0) } } catch (error) { logger.error('Failed to extract JavaScript imports', { error }) return { imports: '', remainingCode: code, importLineCount: 0 } } } /** * Enhanced error information interface */ interface EnhancedError { message: string line?: number column?: number stack?: string name: string originalError: any lineContent?: string } /** * Extract enhanced error information from VM execution errors */ function extractEnhancedError( error: any, userCodeStartLine: number, userCode?: string ): EnhancedError { const enhanced: EnhancedError = { message: error.message || 'Unknown error', name: error.name || 'Error', originalError: error, } if (error.stack) { enhanced.stack = error.stack const stackLines: string[] = error.stack.split('\n') for (const line of stackLines) { let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/) if (!match) { match = line.match(/at\s+user-function\.js:(\d+):(\d+)/) } if (match) { const stackLine = Number.parseInt(match[1], 10) const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined const adjustedLine = stackLine - userCodeStartLine + 1 const isWrapperSyntaxError = stackLine > userCodeStartLine && error.name === 'SyntaxError' && (error.message.includes('Unexpected token') || error.message.includes('Unexpected end of input')) if (isWrapperSyntaxError && userCode) { const codeLines = userCode.split('\n') const lastUserLine = codeLines.length enhanced.line = lastUserLine enhanced.column = codeLines[lastUserLine - 1]?.length || 0 enhanced.lineContent = codeLines[lastUserLine - 1]?.trim() break } if (adjustedLine > 0) { enhanced.line = adjustedLine enhanced.column = stackColumn if (userCode) { const codeLines = userCode.split('\n') if (adjustedLine <= codeLines.length) { enhanced.lineContent = codeLines[adjustedLine - 1]?.trim() } } break } if (stackLine <= userCodeStartLine) { enhanced.line = stackLine enhanced.column = stackColumn break } } } const cleanedStackLines: string[] = stackLines .filter( (line: string) => line.includes('user-function.js') || (!line.includes('vm.js') && !line.includes('internal/')) ) .map((line: string) => line.replace(/\s+at\s+/, ' at ')) if (cleanedStackLines.length > 0) { enhanced.stack = cleanedStackLines.join('\n') } } return enhanced } /** * Parse and format E2B error message * Removes E2B-specific line references and adds correct user line numbers */ function formatE2BError( errorMessage: string, errorOutput: string, language: CodeLanguage, userCode: string, prologueLineCount: number ): { formattedError: string; cleanedOutput: string } { const wrapperLines = language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES const totalOffset = prologueLineCount + wrapperLines let userLine: number | undefined let cleanErrorType = '' let cleanErrorMsg = '' if (language === CodeLanguage.Python) { const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/) if (cellMatch) { const originalLine = Number.parseInt(cellMatch[1], 10) userLine = originalLine - totalOffset } cleanErrorMsg = errorMessage .replace(/\s*\(detected at line \d+\)/g, '') .replace(/\s*\([^)]+\.py, line \d+\)/g, '') .trim() } else if (language === CodeLanguage.JavaScript) { const firstLineEnd = errorMessage.indexOf('\n') const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/) if (jsErrorMatch) { cleanErrorType = jsErrorMatch[1] cleanErrorMsg = jsErrorMatch[2].trim() const originalLine = Number.parseInt(jsErrorMatch[3], 10) userLine = originalLine - totalOffset } else { const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m) if (arrowMatch) { const originalLine = Number.parseInt(arrowMatch[1], 10) userLine = originalLine - totalOffset } const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/) if (errorMatch) { cleanErrorType = errorMatch[1] cleanErrorMsg = errorMatch[2] .replace(/^[^:]+:\s*/, '') // Remove file path .replace(/\s*\(\d+:\d+\)\s*$/, '') // Remove line:col at end .trim() } else { cleanErrorMsg = firstLine } } } const finalErrorMsg = cleanErrorType && cleanErrorMsg ? `${cleanErrorType}: ${cleanErrorMsg}` : cleanErrorMsg || errorMessage let formattedError = finalErrorMsg if (userLine && userLine > 0) { const codeLines = userCode.split('\n') // Clamp userLine to the actual user code range const actualUserLine = Math.min(userLine, codeLines.length) if (actualUserLine > 0 && actualUserLine <= codeLines.length) { const lineContent = codeLines[actualUserLine - 1]?.trim() if (lineContent) { formattedError = `Line ${actualUserLine}: \`${lineContent}\` - ${finalErrorMsg}` } else { formattedError = `Line ${actualUserLine} - ${finalErrorMsg}` } } } const cleanedOutput = finalErrorMsg return { formattedError, cleanedOutput } } /** * Create a detailed error message for users */ function createUserFriendlyErrorMessage( enhanced: EnhancedError, requestId: string, userCode?: string ): string { let errorMessage = enhanced.message if (enhanced.line !== undefined) { let lineInfo = `Line ${enhanced.line}` // Add the actual line content if available if (enhanced.lineContent) { lineInfo += `: \`${enhanced.lineContent}\`` } errorMessage = `${lineInfo} - ${errorMessage}` } else { if (enhanced.stack) { const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/) if (stackMatch) { const line = Number.parseInt(stackMatch[1], 10) let lineInfo = `Line ${line}` if (userCode) { const codeLines = userCode.split('\n') if (line <= codeLines.length) { const lineContent = codeLines[line - 1]?.trim() if (lineContent) { lineInfo += `: \`${lineContent}\`` } } } errorMessage = `${lineInfo} - ${errorMessage}` } } } if (enhanced.name !== 'Error') { const errorTypePrefix = enhanced.name === 'SyntaxError' ? 'Syntax Error' : enhanced.name === 'TypeError' ? 'Type Error' : enhanced.name === 'ReferenceError' ? 'Reference Error' : enhanced.name if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) { errorMessage = `${errorTypePrefix}: ${errorMessage}` } } 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, contextVariables: Record ): string { let resolvedCode = code const regex = createWorkflowVariablePattern() let match: RegExpExecArray | null const replacements: Array<{ match: string index: number variableName: string variableValue: unknown }> = [] while ((match = regex.exec(code)) !== null) { const variableName = match[1].trim() const foundVariable = Object.entries(workflowVariables).find( ([_, variable]) => normalizeName(variable.name || '') === variableName ) if (!foundVariable) { const availableVars = Object.values(workflowVariables) .map((v) => v.name) .filter(Boolean) throw new Error( `Variable "${variableName}" doesn't exist.` + (availableVars.length > 0 ? ` Available: ${availableVars.join(', ')}` : '') ) } const variable = foundVariable[1] let variableValue: unknown = variable.value if (variable.value !== undefined && variable.value !== null) { const type = variable.type === 'string' ? 'plain' : variable.type if (type === 'number') { variableValue = Number(variableValue) } else if (type === 'boolean') { if (typeof variableValue === 'boolean') { // Already a boolean, keep as-is } else { const normalized = String(variableValue).toLowerCase().trim() variableValue = normalized === 'true' } } else if (type === 'json' && typeof variableValue === 'string') { try { variableValue = JSON.parse(variableValue) } catch { // Keep as-is } } } replacements.push({ match: match[0], index: match.index, variableName, variableValue, }) } for (let i = replacements.length - 1; i >= 0; i--) { const { match: matchStr, index, variableName, variableValue } = replacements[i] const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}` contextVariables[safeVarName] = variableValue resolvedCode = resolvedCode.slice(0, index) + safeVarName + resolvedCode.slice(index + matchStr.length) } return resolvedCode } function resolveEnvironmentVariables( code: string, params: Record, envVars: Record, contextVariables: Record ): string { let resolvedCode = code const regex = createEnvVarPattern() let match: RegExpExecArray | null const replacements: Array<{ match: string; index: number; varName: string; varValue: string }> = [] const resolverVars: Record = {} Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { resolverVars[key] = String(value) } }) Object.entries(envVars).forEach(([key, value]) => { if (value !== undefined && value !== null) { resolverVars[key] = value } }) while ((match = regex.exec(code)) !== null) { const varName = match[1].trim() if (!(varName in resolverVars)) { continue } replacements.push({ match: match[0], index: match.index, varName, varValue: resolverVars[varName], }) } for (let i = replacements.length - 1; i >= 0; i--) { const { match: matchStr, index, varName, varValue } = replacements[i] const safeVarName = `__var_${varName.replace(/[^a-zA-Z0-9_]/g, '_')}` contextVariables[safeVarName] = varValue resolvedCode = resolvedCode.slice(0, index) + safeVarName + resolvedCode.slice(index + matchStr.length) } return resolvedCode } function resolveTagVariables( code: string, blockData: Record, blockNameMapping: Record, blockOutputSchemas: Record, contextVariables: Record, language = 'javascript' ): string { let resolvedCode = code const undefinedLiteral = language === 'python' ? 'None' : 'undefined' const tagMatches = resolvedCode.match(TAG_PATTERN) || [] for (const match of tagMatches) { const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim() const pathParts = tagName.split(REFERENCE.PATH_DELIMITER) const blockName = pathParts[0] const fieldPath = pathParts.slice(1) const result = resolveBlockReference(blockName, fieldPath, { blockNameMapping, blockData, blockOutputSchemas, }) if (!result) { continue } let tagValue = result.value if (tagValue === undefined) { resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral) continue } if (typeof tagValue === 'string') { const trimmed = tagValue.trimStart() if (trimmed.startsWith('{') || trimmed.startsWith('[')) { try { tagValue = JSON.parse(tagValue) } catch { // Keep as string if not valid JSON } } } const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}` contextVariables[safeVarName] = tagValue resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName) } return resolvedCode } /** * Resolves environment variables and tags in code * @param code - Code with variables * @param params - Parameters that may contain variable values * @param envVars - Environment variables from the workflow * @returns Resolved code */ function resolveCodeVariables( code: string, params: Record, envVars: Record = {}, blockData: Record = {}, blockNameMapping: Record = {}, blockOutputSchemas: Record = {}, workflowVariables: Record = {}, language = 'javascript' ): { resolvedCode: string; contextVariables: Record } { let resolvedCode = code const contextVariables: Record = {} resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables) resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables) resolvedCode = resolveTagVariables( resolvedCode, blockData, blockNameMapping, blockOutputSchemas, contextVariables, language ) return { resolvedCode, contextVariables } } /** * Remove one trailing newline from stdout * This handles the common case where print() or console.log() adds a trailing \n * that users don't expect to see in the output */ /** * Heuristic: did the sandbox die from an infrastructure failure (OOM kill, * timeout, lost connection) rather than a normal code error? Python/JS code * exceptions surface via execution.error; an OOM kill instead makes runCode * throw, often with an empty or cryptic message. */ function isLikelySandboxKill(error: any): boolean { const msg = `${error?.name ?? ''} ${error?.message ?? ''} ${error?.code ?? ''}` .toLowerCase() .trim() if (!msg) return true return [ 'out of memory', 'oom', 'killed', 'sigkill', 'code 137', 'signal 9', 'terminated', 'econnreset', 'epipe', 'socket hang up', 'connection closed', 'connection reset', 'websocket', 'timed out', 'timeout', 'deadline', ].some((s) => msg.includes(s)) } function cleanStdout(stdout: string): string { if (stdout.endsWith('\n')) { return stdout.slice(0, -1) } return stdout } /** * Serializes a value for use as a shell environment variable. Strings pass through * unchanged; primitives are coerced via `String`; objects, arrays, and other complex * values are JSON-stringified so that referencing them via `$VAR` yields a useful * representation instead of `[object Object]`. `null`/`undefined` become an empty * string to match POSIX env semantics. */ function serializeForShellEnv(value: unknown, nullValue = ''): string { if (value === null || value === undefined) return nullValue if (typeof value === 'string') return value if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { return String(value) } try { return JSON.stringify(value) ?? '' } catch { return String(value) } } interface FunctionRouteExecutionContext { workflowId?: string workspaceId?: string executionId?: string largeValueExecutionIds?: string[] largeValueKeys?: string[] fileKeys?: string[] allowLargeValueWorkflowScope?: boolean userId?: string requestId: string } function asRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} } function getPositiveNumber(value: unknown): number | undefined { if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { return undefined } return value } function clampInlineBytes(value: unknown, limit = MAX_FUNCTION_INLINE_BYTES): number { const requested = getPositiveNumber(value) return Math.min(requested ?? limit, limit) } function getBrokerFileArgs(args: unknown): { file: unknown maxBytes: number offset?: number length?: number } { const record = asRecord(args) const options = asRecord(record.options) return { file: record.file, maxBytes: clampInlineBytes(options.maxBytes), offset: getPositiveNumber(options.offset), length: getPositiveNumber(options.length), } } function createFunctionRuntimeBrokers( context: FunctionRouteExecutionContext ): Record { context.largeValueKeys ??= [] context.fileKeys ??= [] const largeValueKeys = context.largeValueKeys const fileKeys = context.fileKeys const base = { requestId: context.requestId, workflowId: context.workflowId, workspaceId: context.workspaceId, executionId: context.executionId, largeValueExecutionIds: context.largeValueExecutionIds, largeValueKeys, fileKeys, allowLargeValueWorkflowScope: context.allowLargeValueWorkflowScope, userId: context.userId, logger, } const recordMaterializedKeys = (value: unknown) => recordMaterializedAccessKeys({ largeValueKeys, fileKeys }, value) const readFile = async (args: unknown, encoding: 'base64' | 'text', chunked = false) => { const fileArgs = getBrokerFileArgs(args) return readUserFileContent(fileArgs.file, { ...base, encoding, maxBytes: fileArgs.maxBytes, chunked, offset: chunked ? fileArgs.offset : undefined, length: chunked ? fileArgs.length : undefined, }) } return { 'sim.files.readBase64': (args) => readFile(args, 'base64'), 'sim.files.readText': (args) => readFile(args, 'text'), 'sim.files.readBase64Chunk': (args) => readFile(args, 'base64', true), 'sim.files.readTextChunk': (args) => readFile(args, 'text', true), 'sim.values.read': async (args) => { const record = asRecord(args) const options = asRecord(record.options) const ref = record.ref if (!isLargeValueRef(ref)) { throw new Error('Expected a large execution value reference.') } if (!context.executionId) { throw new Error('Large execution values require an execution context.') } const value = await materializeLargeValueRef(ref, { ...base, maxBytes: clampInlineBytes(options.maxBytes, MAX_INLINE_MATERIALIZATION_BYTES), }) if (value === undefined) { throw unavailableLargeValueError(ref) } recordMaterializedKeys(value) return value }, 'sim.values.readArray': async (args) => { const record = asRecord(args) const options = asRecord(record.options) const manifest = record.ref if (!isLargeArrayManifest(manifest)) { throw new Error('Expected a large array manifest.') } if (!context.executionId) { throw new Error('Large array manifests require an execution context.') } const value = await materializeLargeArrayManifest(manifest, { ...base, maxBytes: clampInlineBytes(options.maxBytes, MAX_INLINE_MATERIALIZATION_BYTES), }) recordMaterializedKeys(value) return value }, } } async function compactFunctionRouteBody( body: T, context: FunctionRouteExecutionContext ): Promise { return compactExecutionPayload(body, { workflowId: context.workflowId, workspaceId: context.workspaceId, executionId: context.executionId, userId: context.userId, preserveRoot: true, requireDurable: Boolean(context.workspaceId && context.workflowId && context.executionId), }) } async function functionJsonResponse( body: T, context: FunctionRouteExecutionContext, init?: ResponseInit ) { return NextResponse.json( await compactFunctionRouteBody( { ...body, largeValueKeys: context.largeValueKeys, fileKeys: context.fileKeys, }, context ), init ) } async function maybeExportSandboxFileToWorkspace(args: { authUserId: string workflowId?: string workspaceId?: string outputPath?: string outputFormat?: string outputMimeType?: string outputSandboxPath?: string overwriteFileId?: string outputMode?: 'create' | 'overwrite' exportedFileContent?: string stdout: string executionTime: number }) { const { authUserId, workflowId, workspaceId, outputPath, outputFormat, outputMimeType, outputSandboxPath, overwriteFileId, outputMode, exportedFileContent, stdout, executionTime, } = args if (!outputSandboxPath) return null if (!outputPath) { return NextResponse.json( { success: false, error: 'outputSandboxPath requires outputPath. Set outputPath to the destination workspace file, e.g. "files/result.csv".', output: { result: null, stdout: cleanStdout(stdout), executionTime }, }, { status: 400 } ) } const resolvedWorkspaceId = workspaceId || (workflowId ? (await getWorkflowById(workflowId))?.workspaceId : undefined) if (!resolvedWorkspaceId) { return NextResponse.json( { success: false, error: 'Workspace context required to save sandbox file to workspace', output: { result: null, stdout: cleanStdout(stdout), executionTime }, }, { status: 400 } ) } if (exportedFileContent === undefined) { return NextResponse.json( { success: false, error: `Sandbox file "${outputSandboxPath}" was not found or could not be read`, output: { result: null, stdout: cleanStdout(stdout), executionTime }, }, { status: 500 } ) } const fileName = normalizeOutputWorkspaceFileName(outputPath) const TEXT_MIMES = new Set(Object.values(FORMAT_TO_CONTENT_TYPE)) const resolvedMimeType = outputMimeType || FORMAT_TO_CONTENT_TYPE[resolveOutputFormat(fileName, outputFormat)] || 'application/octet-stream' const isBinary = !TEXT_MIMES.has(resolvedMimeType) const fileBuffer = isBinary ? Buffer.from(exportedFileContent, 'base64') : Buffer.from(exportedFileContent, 'utf-8') const targetPath = overwriteFileId || outputPath const mode = outputMode ?? (overwriteFileId ? 'overwrite' : 'create') try { const written = await writeWorkspaceFileByPath({ workspaceId: resolvedWorkspaceId, userId: authUserId, target: { path: targetPath, mode, mimeType: outputMimeType, }, buffer: fileBuffer, inferredMimeType: resolvedMimeType, }) logger.info('Sandbox file exported to workspace', { fileId: written.id, vfsPath: written.vfsPath, sandboxPath: outputSandboxPath, mode, mimeType: resolvedMimeType, size: fileBuffer.length, }) return NextResponse.json({ success: true, output: { result: { message: `Sandbox file exported to ${written.vfsPath}`, fileId: written.id, fileName: written.name, vfsPath: written.vfsPath, downloadUrl: written.downloadUrl, sandboxPath: outputSandboxPath, }, stdout: cleanStdout(stdout), executionTime, }, resources: [{ type: 'file', id: written.id, title: written.name, path: written.vfsPath }], }) } catch (error) { return NextResponse.json( { success: false, error: getErrorMessage(error, 'Failed to export sandbox file'), output: { result: null, stdout: cleanStdout(stdout), executionTime }, }, { status: 400 } ) } } async function maybeExportSandboxFilesToWorkspace(args: { authUserId: string workflowId?: string workspaceId?: string outputFiles: OutputFileDeclaration[] exportedFiles?: Record exportedFileContent?: string stdout: string executionTime: number }) { const sandboxFiles = args.outputFiles.filter((file) => file.sandboxPath) if (sandboxFiles.length === 0) return null if (sandboxFiles.length > MAX_SANDBOX_OUTPUT_FILES) { return NextResponse.json( { success: false, error: `Too many sandbox output files requested (${sandboxFiles.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`, output: { result: null, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, }, { status: 400 } ) } if (sandboxFiles.length === 1) { const file = sandboxFiles[0] return maybeExportSandboxFileToWorkspace({ authUserId: args.authUserId, workflowId: args.workflowId, workspaceId: args.workspaceId, outputPath: file.formatPath ?? file.path, outputFormat: file.format, outputMimeType: file.mimeType, outputSandboxPath: file.sandboxPath, outputMode: file.mode, exportedFileContent: (file.sandboxPath ? args.exportedFiles?.[file.sandboxPath] : undefined) ?? args.exportedFileContent, stdout: args.stdout, executionTime: args.executionTime, }) } const resolvedWorkspaceId = args.workspaceId || (args.workflowId ? (await getWorkflowById(args.workflowId))?.workspaceId : undefined) if (!resolvedWorkspaceId) { return NextResponse.json( { success: false, error: 'Workspace context required to save sandbox files to workspace', output: { result: null, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, }, { status: 400 } ) } const preparedFiles = [] let totalOutputBytes = 0 for (const file of sandboxFiles) { const sandboxPath = file.sandboxPath! const content = args.exportedFiles?.[sandboxPath] if (content === undefined) { return NextResponse.json( { success: false, error: `Sandbox file "${sandboxPath}" was not found or could not be read`, output: { result: null, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, }, { status: 500 } ) } const fileName = normalizeOutputWorkspaceFileName(file.formatPath ?? file.path) const resolvedMimeType = file.mimeType || FORMAT_TO_CONTENT_TYPE[resolveOutputFormat(fileName, file.format)] || 'application/octet-stream' const isBinary = !new Set(Object.values(FORMAT_TO_CONTENT_TYPE)).has(resolvedMimeType) const size = Buffer.byteLength(content, isBinary ? 'base64' : 'utf-8') totalOutputBytes += size if (totalOutputBytes > MAX_SANDBOX_OUTPUT_BYTES) { return NextResponse.json( { success: false, error: `Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`, output: { result: null, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, }, { status: 400 } ) } preparedFiles.push({ file, sandboxPath, content, resolvedMimeType, isBinary, size, target: { path: file.path, mode: file.mode ?? 'create', mimeType: file.mimeType, }, }) } let validationPaths: string[] try { const validations = await Promise.all( preparedFiles.map((prepared) => validateWorkspaceFileWriteTarget({ workspaceId: resolvedWorkspaceId, userId: args.authUserId, target: prepared.target, }) ) ) validationPaths = validations.map((validation) => validation.vfsPath) } catch (error) { return NextResponse.json( { success: false, error: getErrorMessage(error, 'Invalid sandbox output destination'), output: { result: null, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, }, { status: 400 } ) } const duplicateDestination = validationPaths.find( (vfsPath, index) => validationPaths.indexOf(vfsPath) !== index ) if (duplicateDestination) { return NextResponse.json( { success: false, error: `Duplicate sandbox output destination: ${duplicateDestination}`, output: { result: null, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, }, { status: 400 } ) } const writtenFiles = [] try { for (const prepared of preparedFiles) { const buffer = prepared.isBinary ? Buffer.from(prepared.content, 'base64') : Buffer.from(prepared.content, 'utf-8') const written = await writeWorkspaceFileByPath({ workspaceId: resolvedWorkspaceId, userId: args.authUserId, target: prepared.target, buffer, inferredMimeType: prepared.resolvedMimeType, }) logger.info('Sandbox file exported to workspace', { fileId: written.id, vfsPath: written.vfsPath, sandboxPath: prepared.sandboxPath, mode: prepared.file.mode ?? 'create', mimeType: prepared.resolvedMimeType, size: prepared.size, }) writtenFiles.push({ ...written, sandboxPath: prepared.sandboxPath }) } } catch (error) { return NextResponse.json( { success: false, error: getErrorMessage(error, 'Failed to export sandbox files'), output: { result: null, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, }, { status: 400 } ) } return NextResponse.json({ success: true, output: { result: { message: `Exported ${writtenFiles.length} sandbox files`, files: writtenFiles.map((file) => ({ fileId: file.id, fileName: file.name, vfsPath: file.vfsPath, backingVfsPath: file.backingVfsPath, downloadUrl: file.downloadUrl, sandboxPath: file.sandboxPath, })), }, stdout: cleanStdout(args.stdout), executionTime: args.executionTime, }, resources: writtenFiles.map((file) => ({ type: 'file', id: file.id, title: file.name, path: file.vfsPath, })), }) } export const POST = withRouteHandler(async (req: NextRequest) => { const requestId = generateRequestId() const startTime = Date.now() let stdout = '' let userCodeStartLine = 3 // Default value for error reporting let resolvedCode = '' // Store resolved code for error reporting let sourceCodeForErrors: string | undefined let routeContext: FunctionRouteExecutionContext | undefined try { const auth = await checkInternalAuth(req) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Unauthorized function execution attempt`) return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(functionExecuteContract, req, {}) if (!parsed.success) return parsed.response const { body } = parsed.data const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants') const { code, sourceCode, params = {}, timeout = DEFAULT_EXECUTION_TIMEOUT_MS, language = DEFAULT_CODE_LANGUAGE, outputPath, outputFormat, outputMimeType, outputSandboxPath, overwriteFileId, outputs, envVars = {}, blockData = {}, blockNameMapping = {}, blockOutputSchemas = {}, workflowVariables = {}, contextVariables: preResolvedContextVariables = {}, workflowId, executionId, largeValueExecutionIds, largeValueKeys, fileKeys, allowLargeValueWorkflowScope = false, workspaceId, isCustomTool = false, _sandboxFiles, } = body sourceCodeForErrors = sourceCode const outputFiles = getOutputFileDeclarations({ outputs, outputPath, outputFormat, outputMimeType, outputSandboxPath, overwriteFileId, }) const outputSandboxPaths = outputFiles .map((file) => file.sandboxPath) .filter((path): path is string => Boolean(path)) if (outputSandboxPaths.length > MAX_SANDBOX_OUTPUT_FILES) { return NextResponse.json( { success: false, error: `Too many sandbox output files requested (${outputSandboxPaths.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`, }, { status: 400 } ) } const executionParams = { ...params } executionParams._context = undefined logger.info(`[${requestId}] Function execution request`, { hasCode: !!code, paramsCount: Object.keys(executionParams).length, timeout, workflowId, executionId, isCustomTool, }) routeContext = { workflowId, workspaceId, executionId, largeValueExecutionIds, largeValueKeys, fileKeys, allowLargeValueWorkflowScope, userId: auth.userId, requestId, } const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE let contextVariables: Record = {} if (lang === CodeLanguage.Shell) { // For shell, env vars are injected as OS env vars via shellEnvs. // Replace {{VAR}} placeholders with $VAR so the shell can access them natively. resolvedCode = code.replace(/\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g, '$$$1') // Carry pre-resolved block output variables (e.g. __blockRef_N) so they can be // injected as shell env vars below. The executor replaces block references in the // code with these names, so the values must be present at runtime. contextVariables = { ...preResolvedContextVariables } } else { const codeResolution = resolveCodeVariables( code, executionParams, envVars, blockData, blockNameMapping, blockOutputSchemas, workflowVariables, lang ) resolvedCode = codeResolution.resolvedCode // Merge pre-resolved block output variables from the executor. These take precedence // because they were produced by the resolver using full execution-state context // (including loop/parallel scope) and should not be overwritten. contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables } } if (lang === CodeLanguage.Shell && containsLargeValueRef(contextVariables)) { throw new Error( 'Large execution values require the JavaScript isolated-vm runtime. Select a nested field or read the value in a JavaScript function.' ) } let jsImports = '' let jsRemainingCode = resolvedCode let hasImports = false if (lang === CodeLanguage.JavaScript) { const extractionResult = await extractJavaScriptImports(resolvedCode) jsImports = extractionResult.imports jsRemainingCode = extractionResult.remainingCode const hasRequireStatements = /require\s*\(\s*['"`]/.test(resolvedCode) hasImports = jsImports.trim().length > 0 || hasRequireStatements } if (lang === CodeLanguage.Shell) { if (!isE2bEnabled) { throw new Error( 'Shell execution requires E2B to be enabled. Please contact your administrator to enable E2B.' ) } const shellEnvs: Record = {} for (const [k, v] of Object.entries(envVars)) { shellEnvs[k] = serializeForShellEnv(v) } for (const [k, v] of Object.entries(contextVariables)) { shellEnvs[k] = serializeForShellEnv(v, 'null') } logger.info(`[${requestId}] E2B shell execution`, { enabled: isE2bEnabled, hasApiKey: Boolean(process.env.E2B_API_KEY), envVarCount: Object.keys(shellEnvs).length, }) const execStart = Date.now() const { result: shellResult, stdout: shellStdout, sandboxId, error: shellError, exportedFileContent, exportedFiles, } = await executeShellInE2B({ code: resolvedCode, envs: shellEnvs, timeoutMs: timeout, sandboxFiles: _sandboxFiles, outputSandboxPath, outputSandboxPaths, }) const executionTime = Date.now() - execStart logger.info(`[${requestId}] E2B shell sandbox`, { sandboxId, stdoutPreview: shellStdout?.slice(0, 200), error: shellError, executionTime, }) if (shellError) { return functionJsonResponse( { success: false, error: shellError, output: { result: null, stdout: cleanStdout(shellStdout), executionTime }, }, routeContext, { status: 422 } ) } if (outputSandboxPaths.length > 0 || outputSandboxPath) { const fileExportResponse = await maybeExportSandboxFilesToWorkspace({ authUserId: auth.userId, workflowId, workspaceId, outputFiles, exportedFiles, exportedFileContent, stdout: shellStdout, executionTime, }) if (fileExportResponse) return fileExportResponse } return functionJsonResponse( { success: true, output: { result: shellResult ?? null, stdout: cleanStdout(shellStdout), executionTime }, }, routeContext ) } if (lang === CodeLanguage.Python && !isE2bEnabled) { throw new Error( 'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.' ) } if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) { throw new Error( 'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.' ) } const useE2B = isE2bEnabled && !isCustomTool && (lang === CodeLanguage.Python || (lang === CodeLanguage.JavaScript && hasImports)) if (useE2B && containsLargeValueRef(contextVariables)) { throw new Error( 'Large execution values require the JavaScript isolated-vm runtime. Remove imports, select a nested field, or read the value in a JavaScript function without E2B.' ) } if (useE2B) { logger.info(`[${requestId}] E2B status`, { enabled: isE2bEnabled, hasApiKey: Boolean(process.env.E2B_API_KEY), language: lang, }) let prologue = '' if (lang === CodeLanguage.JavaScript) { let prologueLineCount = 0 const imports = jsImports const remainingCode = jsRemainingCode const importSection: string = imports ? `${imports}\n` : '' const importLineCount = imports ? imports.split('\n').length : 0 const codeBody = remainingCode resolvedCode = importSection ? `${imports}\n\n${codeBody}` : codeBody prologue += `const params = JSON.parse(${JSON.stringify(JSON.stringify(executionParams))});\n` prologueLineCount++ prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { prologue += `globalThis[${JSON.stringify(k)}] = ${formatLiteralForCode(v, 'javascript')};\n` prologue += `const ${k} = globalThis[${JSON.stringify(k)}];\n` prologueLineCount++ prologueLineCount++ } const wrapped = [ ';(async () => {', ' try {', ' const __sim_result = await (async () => {', ` ${codeBody.split('\n').join('\n ')}`, ' })();', " console.log('__SIM_RESULT__=' + JSON.stringify(__sim_result));", ' } catch (error) {', ' console.log(String((error && (error.stack || error.message)) || error));', ' throw error;', ' }', '})();', ].join('\n') const codeForE2B = importSection + prologue + wrapped const execStart = Date.now() const { result: e2bResult, stdout: e2bStdout, sandboxId, error: e2bError, exportedFileContent, exportedFiles, } = await executeInE2B({ code: codeForE2B, language: CodeLanguage.JavaScript, timeoutMs: timeout, sandboxFiles: _sandboxFiles, outputSandboxPath, outputSandboxPaths, }) const executionTime = Date.now() - execStart stdout += e2bStdout logger.info(`[${requestId}] E2B JS sandbox`, { sandboxId, stdoutPreview: e2bStdout?.slice(0, 200), error: e2bError, }) if (e2bError) { const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode) const { formattedError, cleanedOutput } = formatE2BError( getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode), e2bStdout, lang, errorDisplayCode, prologueLineCount + importLineCount ) return functionJsonResponse( { success: false, error: formattedError, output: { result: null, stdout: cleanedOutput, executionTime }, }, routeContext, { status: 422 } ) } if (outputSandboxPaths.length > 0 || outputSandboxPath) { const fileExportResponse = await maybeExportSandboxFilesToWorkspace({ authUserId: auth.userId, workflowId, workspaceId, outputFiles, exportedFiles, exportedFileContent, stdout, executionTime, }) if (fileExportResponse) return fileExportResponse } return functionJsonResponse( { success: true, output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime }, }, routeContext ) } let prologueLineCount = 0 prologue += 'import json\n' prologueLineCount++ prologue += `params = json.loads(${JSON.stringify(JSON.stringify(executionParams))})\n` prologueLineCount++ prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n` prologueLineCount++ } const wrapped = [ 'def __sim_main__():', ...resolvedCode.split('\n').map((l) => ` ${l}`), '__sim_result__ = __sim_main__()', "print('__SIM_RESULT__=' + json.dumps(__sim_result__))", ].join('\n') const codeForE2B = prologue + wrapped const execStart = Date.now() const { result: e2bResult, stdout: e2bStdout, sandboxId, error: e2bError, exportedFileContent, exportedFiles, } = await executeInE2B({ code: codeForE2B, language: CodeLanguage.Python, timeoutMs: timeout, sandboxFiles: _sandboxFiles, outputSandboxPath, outputSandboxPaths, }) const executionTime = Date.now() - execStart stdout += e2bStdout logger.info(`[${requestId}] E2B Py sandbox`, { sandboxId, stdoutPreview: e2bStdout?.slice(0, 200), error: e2bError, }) if (e2bError) { const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode) const { formattedError, cleanedOutput } = formatE2BError( getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode), e2bStdout, lang, errorDisplayCode, prologueLineCount ) return functionJsonResponse( { success: false, error: formattedError, output: { result: null, stdout: cleanedOutput, executionTime }, }, routeContext, { status: 422 } ) } if (outputSandboxPaths.length > 0 || outputSandboxPath) { const fileExportResponse = await maybeExportSandboxFilesToWorkspace({ authUserId: auth.userId, workflowId, workspaceId, outputFiles, exportedFiles, exportedFileContent, stdout, executionTime, }) if (fileExportResponse) return fileExportResponse } return functionJsonResponse( { success: true, output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime }, }, routeContext ) } const executionMethod = 'isolated-vm' const isSafeParamKey = (key: string) => SAFE_IDENTIFIER.test(key) && !JS_RESERVED_WORDS.has(key) const wrapperLines = ['(async () => {', ' try {'] if (isCustomTool) { Object.keys(executionParams).forEach((key) => { if (isSafeParamKey(key)) { wrapperLines.push(` const ${key} = params.${key};`) } else { logger.warn('Skipping param key — not a safe JS identifier', { key, requestId }) } }) } userCodeStartLine = wrapperLines.length + 1 let codeToExecute = resolvedCode let prependedLineCount = 0 if (isCustomTool) { const paramKeys = Object.keys(executionParams).filter(isSafeParamKey) const paramDestructuring = paramKeys.map((key) => `const ${key} = params.${key};`).join('\n') codeToExecute = `${paramDestructuring}\n${resolvedCode}` prependedLineCount = paramKeys.length } const isolatedResult = await executeInIsolatedVM( { code: codeToExecute, params: executionParams, envVars, contextVariables, timeoutMs: timeout, requestId, ownerKey: `user:${auth.userId}`, ownerWeight: 1, }, { brokers: createFunctionRuntimeBrokers(routeContext) } ) const executionTime = Date.now() - startTime if (isolatedResult.error) { const isSystemError = isolatedResult.error.isSystemError === true const logFn = isSystemError ? logger.error.bind(logger) : logger.warn.bind(logger) logFn(`[${requestId}] Function execution failed in isolated-vm`, { error: isolatedResult.error, executionTime, isSystemError, }) const ivmError = isolatedResult.error let adjustedLine = ivmError.line let adjustedLineContent = ivmError.lineContent if (prependedLineCount > 0 && ivmError.line !== undefined) { adjustedLine = Math.max(1, ivmError.line - prependedLineCount) } const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode) const displayMessage = getErrorDisplayMessage( ivmError.message, sourceCodeForErrors, resolvedCode ) adjustedLineContent = getLineContent(errorDisplayCode, adjustedLine) ?? adjustedLineContent const enhancedError: EnhancedError = { message: displayMessage, name: ivmError.name, stack: ivmError.stack, originalError: ivmError, line: adjustedLine, column: ivmError.column, lineContent: adjustedLineContent, } const userFriendlyErrorMessage = createUserFriendlyErrorMessage( enhancedError, requestId, errorDisplayCode ) const detailLogFn = isSystemError ? logger.error.bind(logger) : logger.warn.bind(logger) detailLogFn(`[${requestId}] Enhanced error details`, { originalMessage: ivmError.message, enhancedMessage: userFriendlyErrorMessage, line: enhancedError.line, column: enhancedError.column, lineContent: enhancedError.lineContent, errorType: enhancedError.name, }) return functionJsonResponse( { success: false, error: userFriendlyErrorMessage, output: { result: null, stdout: cleanStdout(isolatedResult.stdout), executionTime, }, debug: { line: enhancedError.line, column: enhancedError.column, errorType: enhancedError.name, lineContent: enhancedError.lineContent, stack: enhancedError.stack, }, }, routeContext, { status: isSystemError ? 500 : 422 } ) } stdout = isolatedResult.stdout logger.info(`[${requestId}] Function executed successfully using ${executionMethod}`, { executionTime, }) return functionJsonResponse( { success: true, output: { result: isolatedResult.result, stdout: cleanStdout(stdout), executionTime }, }, routeContext ) } catch (error: any) { const executionTime = Date.now() - startTime if (isExecutionResourceLimitError(error)) { logger.warn(`[${requestId}] Function execution exceeded resource limits`, { resource: error.resource, attemptedBytes: error.attemptedBytes, limitBytes: error.limitBytes, executionTime, }) if (routeContext) { return functionJsonResponse( { success: false, error: error.message, output: { result: null, stdout: cleanStdout(stdout), executionTime, }, }, routeContext, { status: error.statusCode } ) } return NextResponse.json( { success: false, error: error.message, output: { result: null, stdout: cleanStdout(stdout), executionTime, }, }, { status: error.statusCode } ) } if (isLikelySandboxKill(error)) { const underlying = (error?.message || String(error)).slice(0, 300) logger.warn(`[${requestId}] Sandbox terminated before completion (likely OOM or timeout)`, { executionTime, underlying, }) const killResponse = { success: false, error: 'The sandbox was terminated before finishing — most likely it ran out of memory or hit the time limit while processing large or combined inputs. Mount and process fewer/smaller files at once (e.g. one file at a time), or stream and aggregate incrementally instead of loading everything into memory. ' + `(underlying: ${underlying || 'no detail; sandbox died'})`, output: { result: null, stdout: cleanStdout(stdout), executionTime }, } return routeContext ? functionJsonResponse(killResponse, routeContext, { status: 500 }) : NextResponse.json(killResponse, { status: 500 }) } logger.error(`[${requestId}] Function execution failed`, { error: error.message || 'Unknown error', stack: error.stack, executionTime, }) const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode) const enhancedError = extractEnhancedError(error, userCodeStartLine, errorDisplayCode) const userFriendlyErrorMessage = createUserFriendlyErrorMessage( enhancedError, requestId, errorDisplayCode ) logger.error(`[${requestId}] Enhanced error details`, { originalMessage: error.message, enhancedMessage: userFriendlyErrorMessage, line: enhancedError.line, column: enhancedError.column, lineContent: enhancedError.lineContent, errorType: enhancedError.name, userCodeStartLine, }) const errorResponse = { success: false, error: userFriendlyErrorMessage, output: { result: null, stdout: cleanStdout(stdout), executionTime, }, debug: { line: enhancedError.line, column: enhancedError.column, errorType: enhancedError.name, lineContent: enhancedError.lineContent, stack: enhancedError.stack, }, } if (routeContext) { return functionJsonResponse(errorResponse, routeContext, { status: 500 }) } return NextResponse.json(errorResponse, { status: 500 }) } })