From 4431a1a48486e2049c36873fe9e606468e639f98 Mon Sep 17 00:00:00 2001 From: Martin Yankov <23098926+Lutherwaves@users.noreply.github.com> Date: Sat, 20 Dec 2025 04:59:08 +0200 Subject: [PATCH 1/5] fix(helm): add custom egress rules to realtime network policy (#2481) The realtime service network policy was missing the custom egress rules section that allows configuration of additional egress rules via values.yaml. This caused the realtime pods to be unable to connect to external databases (e.g., PostgreSQL on port 5432) when using external database configurations. The app network policy already had this section, but the realtime network policy was missing it, creating an inconsistency and preventing the realtime service from accessing external databases configured via networkPolicy.egress values. This fix adds the same custom egress rules template section to the realtime network policy, matching the app network policy behavior and allowing users to configure database connectivity via values.yaml. --- helm/sim/templates/networkpolicy.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helm/sim/templates/networkpolicy.yaml b/helm/sim/templates/networkpolicy.yaml index deac5a5dba..7ef8697417 100644 --- a/helm/sim/templates/networkpolicy.yaml +++ b/helm/sim/templates/networkpolicy.yaml @@ -141,6 +141,10 @@ spec: ports: - protocol: TCP port: 443 + # Allow custom egress rules + {{- with .Values.networkPolicy.egress }} + {{- toYaml . | nindent 2 }} + {{- end }} {{- end }} {{- if .Values.postgresql.enabled }} From 189c94aa7d33eb35028477c35c3e5bf1b7626a05 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Mon, 22 Dec 2025 18:56:32 +0800 Subject: [PATCH 2/5] feat(http): add configurable timeout for API block requests Adds timeout parameter to HTTP request tool and API block: - Default timeout: 120000ms (2 minutes) - Max timeout: 600000ms (10 minutes) - User-friendly error message on timeout This allows users to configure longer timeouts for slow API endpoints. Fixes #2242 --- apps/sim/blocks/blocks/api.ts | 9 +++++++++ apps/sim/tools/http/request.ts | 6 ++++++ apps/sim/tools/http/types.ts | 1 + apps/sim/tools/index.ts | 33 ++++++++++++++++++++++++++++++--- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/apps/sim/blocks/blocks/api.ts b/apps/sim/blocks/blocks/api.ts index 50ee12eb3f..5720c85e5d 100644 --- a/apps/sim/blocks/blocks/api.ts +++ b/apps/sim/blocks/blocks/api.ts @@ -80,6 +80,14 @@ Example: generationType: 'json-object', }, }, + { + id: 'timeout', + title: 'Timeout (ms)', + type: 'short-input', + placeholder: '120000', + description: + 'Request timeout in milliseconds. Default: 120000ms (2 min). Max: 600000ms (10 min).', + }, ], tools: { access: ['http_request'], @@ -90,6 +98,7 @@ Example: headers: { type: 'json', description: 'Request headers' }, body: { type: 'json', description: 'Request body data' }, params: { type: 'json', description: 'URL query parameters' }, + timeout: { type: 'number', description: 'Request timeout in milliseconds' }, }, outputs: { data: { type: 'json', description: 'API response data (JSON, text, or other formats)' }, diff --git a/apps/sim/tools/http/request.ts b/apps/sim/tools/http/request.ts index dfb26dd24d..6fbde34947 100644 --- a/apps/sim/tools/http/request.ts +++ b/apps/sim/tools/http/request.ts @@ -40,6 +40,12 @@ export const requestTool: ToolConfig = { type: 'object', description: 'Form data to send (will set appropriate Content-Type)', }, + timeout: { + type: 'number', + default: 120000, + description: + 'Request timeout in milliseconds. Default is 120000ms (2 minutes). Max is 600000ms (10 minutes).', + }, }, request: { diff --git a/apps/sim/tools/http/types.ts b/apps/sim/tools/http/types.ts index aee763469a..feff927f17 100644 --- a/apps/sim/tools/http/types.ts +++ b/apps/sim/tools/http/types.ts @@ -8,6 +8,7 @@ export interface RequestParams { params?: TableRow[] pathParams?: Record formData?: Record + timeout?: number } export interface RequestResponse extends ToolResponse { diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index b4898a6860..45a003ad8b 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -648,14 +648,41 @@ 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 = { + // Determine timeout: use params.timeout if provided, otherwise default to 120000ms (2 min) + // Max timeout is 600000ms (10 minutes) to prevent indefinite waits + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof params.timeout === 'number' && params.timeout > 0) { + timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS) + } else if (typeof params.timeout === 'string') { + const parsed = Number.parseInt(params.timeout, 10) + if (!Number.isNaN(parsed) && parsed > 0) { + timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS) + } + } + + // Prepare request options with timeout signal + const requestOptions: RequestInit = { method: requestParams.method, headers: headers, body: requestParams.body, + signal: AbortSignal.timeout(timeoutMs), } - const response = await fetch(fullUrl, requestOptions) + let response: Response + try { + response = await fetch(fullUrl, requestOptions) + } catch (fetchError) { + // Handle timeout error specifically + if (fetchError instanceof Error && fetchError.name === 'TimeoutError') { + logger.error(`[${requestId}] Request timed out for ${toolId} after ${timeoutMs}ms`) + throw new Error( + `Request timed out after ${timeoutMs}ms. Consider increasing the timeout value.` + ) + } + throw fetchError + } // For non-OK responses, attempt JSON first; if parsing fails, fall back to text if (!response.ok) { From 02a502f441bb1940d5330155ad8b6ea93ef81868 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Tue, 23 Dec 2025 21:21:42 +0800 Subject: [PATCH 3/5] fix(http): add timeout support to proxy requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation only added timeout to handleInternalRequest, but external API requests go through handleProxyRequest which was missing the timeout logic. This caused the proxy fetch to use default timeout instead of the user-configured value. - Add timeout parsing logic to handleProxyRequest - Add AbortSignal.timeout to proxy fetch call - Add user-friendly timeout error message Fixes the issue reported by @ARNOLDAJEE where 600000ms timeout setting was not taking effect for external API requests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/sim/tools/index.ts | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 45a003ad8b..bfad82759f 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -893,11 +893,38 @@ async function handleProxyRequest( // Check request body size before sending validateRequestBodySize(body, requestId, `proxy:${toolId}`) - const response = await fetch(proxyUrl, { - method: 'POST', - headers, - body, - }) + // Determine timeout for proxy request: use params.timeout if provided, otherwise default + // This ensures the proxy fetch itself doesn't timeout before the actual API request + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof params.timeout === 'number' && params.timeout > 0) { + timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS) + } else if (typeof params.timeout === 'string') { + const parsed = Number.parseInt(params.timeout, 10) + if (!Number.isNaN(parsed) && parsed > 0) { + timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS) + } + } + + let response: Response + try { + response = await fetch(proxyUrl, { + method: 'POST', + headers, + body, + signal: AbortSignal.timeout(timeoutMs), + }) + } catch (fetchError) { + // Handle timeout error specifically + if (fetchError instanceof Error && fetchError.name === 'TimeoutError') { + logger.error(`[${requestId}] Proxy request timed out for ${toolId} after ${timeoutMs}ms`) + throw new Error( + `Request timed out after ${timeoutMs}ms. Consider increasing the timeout value.` + ) + } + throw fetchError + } if (!response.ok) { // Check for 413 (Entity Too Large) - body size limit exceeded From a6dc7b18d5950d5f9b83d2add9ffe605e58ec4dc Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Tue, 23 Dec 2025 21:28:20 +0800 Subject: [PATCH 4/5] test(http): add unit tests for timeout parsing logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive unit tests to verify: - Numeric timeout parsing - String timeout parsing - MAX_TIMEOUT_MS cap (600000ms) - DEFAULT_TIMEOUT_MS fallback (120000ms) - Invalid value handling - AbortSignal.timeout integration - Timeout error identification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/sim/tools/timeout.test.ts | 222 +++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 apps/sim/tools/timeout.test.ts diff --git a/apps/sim/tools/timeout.test.ts b/apps/sim/tools/timeout.test.ts new file mode 100644 index 0000000000..82a6e89a68 --- /dev/null +++ b/apps/sim/tools/timeout.test.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +/** + * Tests for timeout functionality in handleProxyRequest and handleInternalRequest + */ +describe('HTTP Timeout Support', () => { + const originalFetch = global.fetch + + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + global.fetch = originalFetch + vi.useRealTimers() + vi.restoreAllMocks() + }) + + describe('Timeout Parameter Parsing', () => { + it('should parse numeric timeout correctly', () => { + const params = { timeout: 5000 } + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof params.timeout === 'number' && params.timeout > 0) { + timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS) + } + + expect(timeoutMs).toBe(5000) + }) + + it('should parse string timeout correctly', () => { + const params = { timeout: '30000' } + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof params.timeout === 'number' && params.timeout > 0) { + timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS) + } else if (typeof params.timeout === 'string') { + const parsed = Number.parseInt(params.timeout, 10) + if (!Number.isNaN(parsed) && parsed > 0) { + timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS) + } + } + + expect(timeoutMs).toBe(30000) + }) + + it('should cap timeout at MAX_TIMEOUT_MS', () => { + const params = { timeout: 1000000 } // 1000 seconds, exceeds max + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof params.timeout === 'number' && params.timeout > 0) { + timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS) + } + + expect(timeoutMs).toBe(MAX_TIMEOUT_MS) + }) + + it('should use default timeout when no timeout provided', () => { + const params = {} + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof (params as any).timeout === 'number' && (params as any).timeout > 0) { + timeoutMs = Math.min((params as any).timeout, MAX_TIMEOUT_MS) + } + + expect(timeoutMs).toBe(DEFAULT_TIMEOUT_MS) + }) + + it('should use default timeout for invalid string', () => { + const params = { timeout: 'invalid' } + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof params.timeout === 'number' && params.timeout > 0) { + timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS) + } else if (typeof params.timeout === 'string') { + const parsed = Number.parseInt(params.timeout, 10) + if (!Number.isNaN(parsed) && parsed > 0) { + timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS) + } + } + + expect(timeoutMs).toBe(DEFAULT_TIMEOUT_MS) + }) + + it('should use default timeout for zero or negative values', () => { + const testCases = [{ timeout: 0 }, { timeout: -1000 }, { timeout: '0' }, { timeout: '-500' }] + const DEFAULT_TIMEOUT_MS = 120000 + const MAX_TIMEOUT_MS = 600000 + + for (const params of testCases) { + let timeoutMs = DEFAULT_TIMEOUT_MS + if (typeof params.timeout === 'number' && params.timeout > 0) { + timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS) + } else if (typeof params.timeout === 'string') { + const parsed = Number.parseInt(params.timeout, 10) + if (!Number.isNaN(parsed) && parsed > 0) { + timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS) + } + } + + expect(timeoutMs).toBe(DEFAULT_TIMEOUT_MS) + } + }) + }) + + describe('AbortSignal.timeout Integration', () => { + it('should create AbortSignal with correct timeout', () => { + const timeoutMs = 5000 + const signal = AbortSignal.timeout(timeoutMs) + + expect(signal).toBeDefined() + expect(signal.aborted).toBe(false) + }) + + it('should abort after timeout period', async () => { + vi.useRealTimers() // Need real timers for this test + + const timeoutMs = 100 // Very short timeout for testing + const signal = AbortSignal.timeout(timeoutMs) + + // Wait for timeout to trigger + await new Promise((resolve) => setTimeout(resolve, timeoutMs + 50)) + + expect(signal.aborted).toBe(true) + }) + }) + + describe('Timeout Error Handling', () => { + it('should identify TimeoutError correctly', () => { + const timeoutError = new Error('The operation was aborted') + timeoutError.name = 'TimeoutError' + + const isTimeoutError = + timeoutError instanceof Error && timeoutError.name === 'TimeoutError' + + expect(isTimeoutError).toBe(true) + }) + + it('should generate user-friendly timeout message', () => { + const timeoutMs = 5000 + const errorMessage = `Request timed out after ${timeoutMs}ms. Consider increasing the timeout value.` + + expect(errorMessage).toBe( + 'Request timed out after 5000ms. Consider increasing the timeout value.' + ) + }) + }) + + describe('Fetch with Timeout Signal', () => { + it('should pass signal to fetch options', async () => { + vi.useRealTimers() + + const mockFetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) + global.fetch = mockFetch + + const timeoutMs = 5000 + await fetch('https://example.com/api', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ test: true }), + signal: AbortSignal.timeout(timeoutMs), + }) + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ + signal: expect.any(AbortSignal), + }) + ) + }) + + it('should throw TimeoutError when request times out', async () => { + vi.useRealTimers() + + // Mock a slow fetch that will be aborted + global.fetch = vi.fn().mockImplementation( + (_url: string, options: RequestInit) => + new Promise((_resolve, reject) => { + if (options?.signal) { + options.signal.addEventListener('abort', () => { + const error = new Error('The operation was aborted') + error.name = 'TimeoutError' + reject(error) + }) + } + }) + ) + + const timeoutMs = 100 + let caughtError: Error | null = null + + try { + await fetch('https://example.com/slow-api', { + signal: AbortSignal.timeout(timeoutMs), + }) + } catch (error) { + caughtError = error as Error + } + + // Wait a bit for the timeout to trigger + await new Promise((resolve) => setTimeout(resolve, timeoutMs + 50)) + + expect(caughtError).not.toBeNull() + expect(caughtError?.name).toBe('TimeoutError') + }) + }) +}) From 4d89f651ce8f7062a22c150214c16e18379af1b5 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Tue, 23 Dec 2025 21:47:20 +0800 Subject: [PATCH 5/5] style: fix lint issues in timeout tests --- apps/sim/tools/timeout.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/sim/tools/timeout.test.ts b/apps/sim/tools/timeout.test.ts index 82a6e89a68..464d1a72d9 100644 --- a/apps/sim/tools/timeout.test.ts +++ b/apps/sim/tools/timeout.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' /** * Tests for timeout functionality in handleProxyRequest and handleInternalRequest @@ -140,8 +140,7 @@ describe('HTTP Timeout Support', () => { const timeoutError = new Error('The operation was aborted') timeoutError.name = 'TimeoutError' - const isTimeoutError = - timeoutError instanceof Error && timeoutError.name === 'TimeoutError' + const isTimeoutError = timeoutError instanceof Error && timeoutError.name === 'TimeoutError' expect(isTimeoutError).toBe(true) })