Files
sim/apps/sim/executor/handlers/api/api-handler.ts
Waleed 0ca25bbab6 fix(function): isolated-vm worker pool to prevent single-worker bottleneck + execution user id resolution (#3155)
* fix(executor): isolated-vm worker pool to prevent single-worker bottleneck

* chore(helm): add isolated-vm worker pool env vars to values.yaml

* fix(userid): resolution for fair scheduling

* add fallback back

* add to helm charts

* remove constant fallbacks

* fix

* address bugbot comments

* fix fallbacks

* one more bugbot comment

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-02-06 18:34:03 -08:00

166 lines
5.7 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { BlockType, HTTP } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
import { executeTool } from '@/tools'
import { getTool } from '@/tools/utils'
const logger = createLogger('ApiBlockHandler')
/**
* Handler for API blocks that make external HTTP requests.
*/
export class ApiBlockHandler implements BlockHandler {
canHandle(block: SerializedBlock): boolean {
return block.metadata?.id === BlockType.API
}
async execute(
ctx: ExecutionContext,
block: SerializedBlock,
inputs: Record<string, any>
): Promise<any> {
const tool = getTool(block.config.tool)
if (!tool) {
throw new Error(`Tool not found: ${block.config.tool}`)
}
if (tool.name?.includes('HTTP') && (!inputs.url || inputs.url.trim() === '')) {
return { data: null, status: HTTP.STATUS.OK, headers: {} }
}
if (tool.name?.includes('HTTP') && inputs.url) {
let urlToValidate = inputs.url
if (typeof urlToValidate === 'string') {
if (
(urlToValidate.startsWith('"') && urlToValidate.endsWith('"')) ||
(urlToValidate.startsWith("'") && urlToValidate.endsWith("'"))
) {
urlToValidate = urlToValidate.slice(1, -1)
inputs.url = urlToValidate
}
}
const urlValidation = await validateUrlWithDNS(urlToValidate, 'url')
if (!urlValidation.isValid) {
throw new Error(urlValidation.error)
}
}
try {
const processedInputs = { ...inputs }
if (processedInputs.body !== undefined) {
if (typeof processedInputs.body === 'string') {
try {
const trimmedBody = processedInputs.body.trim()
if (trimmedBody.startsWith('{') || trimmedBody.startsWith('[')) {
processedInputs.body = JSON.parse(trimmedBody)
}
} catch (e) {}
} else if (processedInputs.body === null) {
processedInputs.body = undefined
}
}
const result = await executeTool(
block.config.tool,
{
...processedInputs,
_context: {
workflowId: ctx.workflowId,
workspaceId: ctx.workspaceId,
executionId: ctx.executionId,
userId: ctx.userId,
isDeployedContext: ctx.isDeployedContext,
},
},
false,
ctx
)
if (!result.success) {
const errorDetails = []
if (inputs.url) errorDetails.push(`URL: ${inputs.url}`)
if (inputs.method) errorDetails.push(`Method: ${inputs.method}`)
if (result.error) errorDetails.push(`Error: ${result.error}`)
if (result.output?.status) errorDetails.push(`Status: ${result.output.status}`)
if (result.output?.statusText) errorDetails.push(`Status text: ${result.output.statusText}`)
let suggestion = ''
if (result.output?.status === HTTP.STATUS.FORBIDDEN) {
suggestion = ' - This may be due to CORS restrictions or authorization issues'
} else if (result.output?.status === HTTP.STATUS.NOT_FOUND) {
suggestion = ' - The requested resource was not found'
} else if (result.output?.status === HTTP.STATUS.TOO_MANY_REQUESTS) {
suggestion = ' - Too many requests, you may need to implement rate limiting'
} else if (result.output?.status >= HTTP.STATUS.SERVER_ERROR) {
suggestion = ' - Server error, the target server is experiencing issues'
} else if (result.error?.includes('CORS')) {
suggestion =
' - CORS policy prevented the request, try using a proxy or server-side request'
} else if (result.error?.includes('Failed to fetch')) {
suggestion =
' - Network error, check if the URL is accessible and if you have internet connectivity'
}
const errorMessage =
errorDetails.length > 0
? `HTTP Request failed: ${errorDetails.join(' | ')}${suggestion}`
: `API request to ${tool.name || block.config.tool} failed with no error message`
const error = new Error(errorMessage)
Object.assign(error, {
toolId: block.config.tool,
toolName: tool.name || 'Unknown tool',
blockId: block.id,
blockName: block.metadata?.name || 'Unnamed Block',
output: result.output || {},
status: result.output?.status || null,
request: {
url: inputs.url,
method: inputs.method || 'GET',
},
timestamp: new Date().toISOString(),
})
throw error
}
return result.output
} catch (error: any) {
if (!error.message || error.message === 'undefined (undefined)') {
let errorMessage = `API request to ${tool.name || block.config.tool} failed`
if (inputs.url) errorMessage += `: ${inputs.url}`
if (error.status) errorMessage += ` (Status: ${error.status})`
if (error.statusText) errorMessage += ` - ${error.statusText}`
if (errorMessage === `API request to ${tool.name || block.config.tool} failed`) {
errorMessage += ` - ${block.metadata?.name || 'Unknown error'}`
}
error.message = errorMessage
}
if (typeof error === 'object' && error !== null) {
if (!error.toolId) error.toolId = block.config.tool
if (!error.blockName) error.blockName = block.metadata?.name || 'Unnamed Block'
if (inputs && !error.request) {
error.request = {
url: inputs.url,
method: inputs.method || 'GET',
}
}
}
throw error
}
}
}