diff --git a/apps/sim/app/api/proxy/route.ts b/apps/sim/app/api/proxy/route.ts index 24702aa48f..f4dddbd56f 100644 --- a/apps/sim/app/api/proxy/route.ts +++ b/apps/sim/app/api/proxy/route.ts @@ -13,6 +13,13 @@ import { getTool, validateRequiredParametersAfterMerge } from '@/tools/utils' const logger = createLogger('ProxyAPI') +/** + * Next.js route configuration for long-running API requests + * maxDuration: 10 minutes to support user-configurable timeouts up to 10 min + */ +export const runtime = 'nodejs' +export const maxDuration = 600 + const proxyPostSchema = z.object({ toolId: z.string().min(1, 'toolId is required'), params: z.record(z.any()).optional().default({}), @@ -212,6 +219,8 @@ export async function GET(request: Request) { try { const pinnedUrl = createPinnedUrl(targetUrl, urlValidation.resolvedIP!) + // timeout: false disables Bun/Node.js default 5-minute timeout + // maxDuration handles the overall route timeout const response = await fetch(pinnedUrl, { method: method, headers: { @@ -220,6 +229,8 @@ export async function GET(request: Request) { Host: urlValidation.originalHostname!, }, body: body || undefined, + // @ts-expect-error - Bun-specific option to disable default 5-minute timeout + timeout: false, }) const contentType = response.headers.get('content-type') || '' diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 5d1a7d7a02..7da5686737 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -50,6 +50,8 @@ const ExecuteWorkflowSchema = z.object({ export const runtime = 'nodejs' export const dynamic = 'force-dynamic' +// Allow up to 10 minutes for workflow execution with long-running API calls +export const maxDuration = 600 function resolveOutputIds( selectedOutputs: string[] | undefined, diff --git a/apps/sim/tools/http/types.ts b/apps/sim/tools/http/types.ts index aee763469a..42bbba983d 100644 --- a/apps/sim/tools/http/types.ts +++ b/apps/sim/tools/http/types.ts @@ -8,6 +8,12 @@ export interface RequestParams { params?: TableRow[] pathParams?: Record formData?: Record + /** + * Request timeout in milliseconds. + * Default: 120000 (2 minutes) + * Maximum: 600000 (10 minutes) + */ + timeout?: number } export interface RequestResponse extends ToolResponse { diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index b0f6f6fd36..2f06d210d5 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -43,6 +43,34 @@ function normalizeToolId(toolId: string): string { */ const MAX_REQUEST_BODY_SIZE_BYTES = 10 * 1024 * 1024 // 10MB +/** + * Default timeout for HTTP requests in milliseconds (2 minutes) + */ +const DEFAULT_TIMEOUT_MS = 120000 + +/** + * Maximum allowed timeout for HTTP requests in milliseconds (10 minutes) + */ +const MAX_TIMEOUT_MS = 600000 + +/** + * Parses and validates a timeout value, ensuring it falls within acceptable bounds. + * @param timeout - The timeout value to parse (number or string in ms) + * @returns The parsed timeout in milliseconds, clamped to MAX_TIMEOUT_MS + */ +function parseTimeout(timeout: number | string | undefined): number { + if (typeof timeout === 'number' && timeout > 0) { + return Math.min(timeout, MAX_TIMEOUT_MS) + } + if (typeof timeout === 'string') { + const parsed = Number.parseInt(timeout, 10) + if (!Number.isNaN(parsed) && parsed > 0) { + return Math.min(parsed, MAX_TIMEOUT_MS) + } + } + return DEFAULT_TIMEOUT_MS +} + /** * User-friendly error message for body size limit exceeded */ @@ -650,11 +678,20 @@ async function handleInternalRequest( // Check request body size before sending to detect potential size limit issues validateRequestBodySize(requestParams.body, requestId, toolId) - // Prepare request options - const requestOptions = { + // Parse timeout from params (user-configurable via API block) + const timeoutMs = parseTimeout(params.timeout) + logger.info(`[${requestId}] Request timeout for ${toolId}: ${timeoutMs}ms`) + + // Prepare request options with timeout support + // Note: timeout: false disables Bun/Node.js default 5-minute timeout + // AbortSignal.timeout provides user-configurable timeout control + const requestOptions: RequestInit & { timeout?: boolean } = { method: requestParams.method, headers: headers, body: requestParams.body, + // @ts-ignore - Bun-specific option to disable default 5-minute timeout (not in standard RequestInit types) + timeout: false, + signal: AbortSignal.timeout(timeoutMs), } const response = await fetch(fullUrl, requestOptions) @@ -870,10 +907,19 @@ async function handleProxyRequest( // Check request body size before sending validateRequestBodySize(body, requestId, `proxy:${toolId}`) + // Parse timeout from params (user-configurable via API block) + const timeoutMs = parseTimeout(params.timeout) + logger.info(`[${requestId}] Proxy request timeout for ${toolId}: ${timeoutMs}ms`) + + // Note: timeout: false disables Bun/Node.js default 5-minute timeout + // AbortSignal.timeout provides user-configurable timeout control const response = await fetch(proxyUrl, { method: 'POST', headers, body, + // @ts-ignore - Bun-specific option to disable default 5-minute timeout (not in standard RequestInit types) + timeout: false, + signal: AbortSignal.timeout(timeoutMs), }) if (!response.ok) {