Skip to content

Commit 06d7ce7

Browse files
authored
feat(timeout): add API block timeout configuration (#3053)
* feat(timeout): add timeout subblock to the api block * fix(timeout): honor timeout config for internal routes and fix type coercion - Add AbortController support for internal routes (/api/*) to honor timeout - Fix type coercion: convert string timeout from short-input to number - Handle NaN gracefully by falling back to undefined (default timeout) Fixes #2786 Fixes #2242 * fix: remove redundant clearTimeout in catch block * fix: validate timeout is positive number Negative timeout values would cause immediate request abort since JavaScript treats negative setTimeout delays as 0. * update docs image, update search modal performance * removed unused keywords type * ack comments * cleanup * fix: add default timeout for internal routes and validate finite timeout - Internal routes now use same 5-minute default as external routes - Added Number.isFinite() check to reject Infinity values * fix: enforce max timeout and improve error message consistency - Clamp timeout to max 600000ms (10 minutes) as documented - External routes now report timeout value in error message * remove unused code
1 parent 1bc476f commit 06d7ce7

File tree

11 files changed

+139
-129
lines changed

11 files changed

+139
-129
lines changed
-90.8 KB
Loading

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -180,20 +180,6 @@ function resolveCustomToolFromReference(
180180
return null
181181
}
182182

183-
/**
184-
* Checks if a stored custom tool uses the reference-only format.
185-
*
186-
* @remarks
187-
* Reference-only format means the tool has a customToolId but no inline code/schema,
188-
* requiring resolution from the database at runtime.
189-
*
190-
* @param storedTool - The stored tool to check
191-
* @returns `true` if the tool is a reference-only custom tool, `false` otherwise
192-
*/
193-
function isCustomToolReference(storedTool: StoredTool): boolean {
194-
return storedTool.type === 'custom-tool' && !!storedTool.customToolId && !storedTool.code
195-
}
196-
197183
/**
198184
* Generic sync wrapper that synchronizes store values with local component state.
199185
*
@@ -1155,21 +1141,6 @@ export const ToolInput = memo(function ToolInput({
11551141
return filterBlocks(allToolBlocks)
11561142
}, [filterBlocks])
11571143

1158-
const customFilter = useCallback((value: string, search: string) => {
1159-
if (!search.trim()) return 1
1160-
1161-
const normalizedValue = value.toLowerCase()
1162-
const normalizedSearch = search.toLowerCase()
1163-
1164-
if (normalizedValue === normalizedSearch) return 1
1165-
1166-
if (normalizedValue.startsWith(normalizedSearch)) return 0.8
1167-
1168-
if (normalizedValue.includes(normalizedSearch)) return 0.6
1169-
1170-
return 0
1171-
}, [])
1172-
11731144
const hasBackfilledRef = useRef(false)
11741145
useEffect(() => {
11751146
if (

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx

Lines changed: 74 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { Database, HelpCircle, Layout, Settings } from 'lucide-react'
66
import { useParams, useRouter } from 'next/navigation'
77
import { createPortal } from 'react-dom'
88
import { Library } from '@/components/emcn'
9-
import { useBrandConfig } from '@/lib/branding/branding'
109
import { cn } from '@/lib/core/utils/cn'
1110
import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
1211
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
12+
import { usePermissionConfig } from '@/hooks/use-permission-config'
1313
import { useSearchModalStore } from '@/stores/modals/search/store'
1414
import type {
1515
SearchBlockItem,
@@ -18,6 +18,23 @@ import type {
1818
} from '@/stores/modals/search/types'
1919
import { useSettingsModalStore } from '@/stores/modals/settings/store'
2020

21+
function customFilter(value: string, search: string): number {
22+
const searchLower = search.toLowerCase()
23+
const valueLower = value.toLowerCase()
24+
25+
if (valueLower === searchLower) return 1
26+
if (valueLower.startsWith(searchLower)) return 0.9
27+
if (valueLower.includes(searchLower)) return 0.7
28+
29+
const searchWords = searchLower.split(/\s+/).filter(Boolean)
30+
if (searchWords.length > 1) {
31+
const allWordsMatch = searchWords.every((word) => valueLower.includes(word))
32+
if (allWordsMatch) return 0.5
33+
}
34+
35+
return 0
36+
}
37+
2138
interface SearchModalProps {
2239
open: boolean
2340
onOpenChange: (open: boolean) => void
@@ -48,6 +65,7 @@ interface PageItem {
4865
href?: string
4966
onClick?: () => void
5067
shortcut?: string
68+
hidden?: boolean
5169
}
5270

5371
export function SearchModal({
@@ -60,11 +78,10 @@ export function SearchModal({
6078
const params = useParams()
6179
const router = useRouter()
6280
const workspaceId = params.workspaceId as string
63-
const brand = useBrandConfig()
6481
const inputRef = useRef<HTMLInputElement>(null)
65-
const [search, setSearch] = useState('')
6682
const [mounted, setMounted] = useState(false)
6783
const openSettingsModal = useSettingsModalStore((state) => state.openModal)
84+
const { config: permissionConfig } = usePermissionConfig()
6885

6986
useEffect(() => {
7087
setMounted(true)
@@ -79,54 +96,66 @@ export function SearchModal({
7996
}, [])
8097

8198
const pages = useMemo(
82-
(): PageItem[] => [
83-
{
84-
id: 'logs',
85-
name: 'Logs',
86-
icon: Library,
87-
href: `/workspace/${workspaceId}/logs`,
88-
shortcut: '⌘⇧L',
89-
},
90-
{
91-
id: 'templates',
92-
name: 'Templates',
93-
icon: Layout,
94-
href: `/workspace/${workspaceId}/templates`,
95-
},
96-
{
97-
id: 'knowledge-base',
98-
name: 'Knowledge Base',
99-
icon: Database,
100-
href: `/workspace/${workspaceId}/knowledge`,
101-
},
102-
{
103-
id: 'help',
104-
name: 'Help',
105-
icon: HelpCircle,
106-
onClick: openHelpModal,
107-
},
108-
{
109-
id: 'settings',
110-
name: 'Settings',
111-
icon: Settings,
112-
onClick: openSettingsModal,
113-
shortcut: '⌘,',
114-
},
115-
],
116-
[workspaceId, openHelpModal, openSettingsModal]
99+
(): PageItem[] =>
100+
[
101+
{
102+
id: 'logs',
103+
name: 'Logs',
104+
icon: Library,
105+
href: `/workspace/${workspaceId}/logs`,
106+
shortcut: '⌘⇧L',
107+
},
108+
{
109+
id: 'templates',
110+
name: 'Templates',
111+
icon: Layout,
112+
href: `/workspace/${workspaceId}/templates`,
113+
hidden: permissionConfig.hideTemplates,
114+
},
115+
{
116+
id: 'knowledge-base',
117+
name: 'Knowledge Base',
118+
icon: Database,
119+
href: `/workspace/${workspaceId}/knowledge`,
120+
hidden: permissionConfig.hideKnowledgeBaseTab,
121+
},
122+
{
123+
id: 'help',
124+
name: 'Help',
125+
icon: HelpCircle,
126+
onClick: openHelpModal,
127+
},
128+
{
129+
id: 'settings',
130+
name: 'Settings',
131+
icon: Settings,
132+
onClick: openSettingsModal,
133+
},
134+
].filter((page) => !page.hidden),
135+
[
136+
workspaceId,
137+
openHelpModal,
138+
openSettingsModal,
139+
permissionConfig.hideTemplates,
140+
permissionConfig.hideKnowledgeBaseTab,
141+
]
117142
)
118143

119144
useEffect(() => {
120-
if (open) {
121-
setSearch('')
122-
requestAnimationFrame(() => {
123-
inputRef.current?.focus()
124-
})
145+
if (open && inputRef.current) {
146+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
147+
window.HTMLInputElement.prototype,
148+
'value'
149+
)?.set
150+
if (nativeInputValueSetter) {
151+
nativeInputValueSetter.call(inputRef.current, '')
152+
inputRef.current.dispatchEvent(new Event('input', { bubbles: true }))
153+
}
154+
inputRef.current.focus()
125155
}
126156
}, [open])
127157

128-
const handleSearchChange = useCallback((value: string) => {
129-
setSearch(value)
158+
const handleSearchChange = useCallback(() => {
130159
requestAnimationFrame(() => {
131160
const list = document.querySelector('[cmdk-list]')
132161
if (list) {
@@ -228,28 +257,6 @@ export function SearchModal({
228257
const showToolOperations = isOnWorkflowPage && toolOperations.length > 0
229258
const showDocs = isOnWorkflowPage && docs.length > 0
230259

231-
const customFilter = useCallback((value: string, search: string, keywords?: string[]) => {
232-
const searchLower = search.toLowerCase()
233-
const valueLower = value.toLowerCase()
234-
235-
if (valueLower === searchLower) return 1
236-
if (valueLower.startsWith(searchLower)) return 0.8
237-
if (valueLower.includes(searchLower)) return 0.6
238-
239-
const searchWords = searchLower.split(/\s+/).filter(Boolean)
240-
const allWordsMatch = searchWords.every((word) => valueLower.includes(word))
241-
if (allWordsMatch && searchWords.length > 0) return 0.4
242-
243-
if (keywords?.length) {
244-
const keywordsLower = keywords.join(' ').toLowerCase()
245-
if (keywordsLower.includes(searchLower)) return 0.3
246-
const keywordWordsMatch = searchWords.every((word) => keywordsLower.includes(word))
247-
if (keywordWordsMatch && searchWords.length > 0) return 0.2
248-
}
249-
250-
return 0
251-
}, [])
252-
253260
if (!mounted) return null
254261

255262
return createPortal(
@@ -278,7 +285,6 @@ export function SearchModal({
278285
<Command label='Search' filter={customFilter}>
279286
<Command.Input
280287
ref={inputRef}
281-
value={search}
282288
autoFocus
283289
onValueChange={handleSearchChange}
284290
placeholder='Search anything...'
@@ -295,7 +301,6 @@ export function SearchModal({
295301
<CommandItem
296302
key={block.id}
297303
value={`${block.name} block-${block.id}`}
298-
keywords={[block.description]}
299304
onSelect={() => handleBlockSelect(block, 'block')}
300305
icon={block.icon}
301306
bgColor={block.bgColor}
@@ -313,7 +318,6 @@ export function SearchModal({
313318
<CommandItem
314319
key={tool.id}
315320
value={`${tool.name} tool-${tool.id}`}
316-
keywords={[tool.description]}
317321
onSelect={() => handleBlockSelect(tool, 'tool')}
318322
icon={tool.icon}
319323
bgColor={tool.bgColor}
@@ -331,7 +335,6 @@ export function SearchModal({
331335
<CommandItem
332336
key={trigger.id}
333337
value={`${trigger.name} trigger-${trigger.id}`}
334-
keywords={[trigger.description]}
335338
onSelect={() => handleBlockSelect(trigger, 'trigger')}
336339
icon={trigger.icon}
337340
bgColor={trigger.bgColor}
@@ -371,7 +374,6 @@ export function SearchModal({
371374
<CommandItem
372375
key={op.id}
373376
value={`${op.searchValue} operation-${op.id}`}
374-
keywords={op.keywords}
375377
onSelect={() => handleToolOperationSelect(op)}
376378
icon={op.icon}
377379
bgColor={op.bgColor}
@@ -458,7 +460,6 @@ const groupHeadingClassName =
458460

459461
interface CommandItemProps {
460462
value: string
461-
keywords?: string[]
462463
onSelect: () => void
463464
icon: React.ComponentType<{ className?: string }>
464465
bgColor: string
@@ -468,7 +469,6 @@ interface CommandItemProps {
468469

469470
function CommandItem({
470471
value,
471-
keywords,
472472
onSelect,
473473
icon: Icon,
474474
bgColor,
@@ -478,7 +478,6 @@ function CommandItem({
478478
return (
479479
<Command.Item
480480
value={value}
481-
keywords={keywords}
482481
onSelect={onSelect}
483482
className='group flex h-[28px] w-full cursor-pointer items-center gap-[8px] rounded-[6px] px-[10px] text-left text-[15px] aria-selected:bg-[var(--border)] aria-selected:shadow-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50'
484483
>

apps/sim/blocks/blocks/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ Example:
8080
generationType: 'json-object',
8181
},
8282
},
83+
{
84+
id: 'timeout',
85+
title: 'Timeout (ms)',
86+
type: 'short-input',
87+
placeholder: '300000',
88+
description:
89+
'Request timeout in milliseconds (default: 300000 = 5 minutes, max: 600000 = 10 minutes)',
90+
mode: 'advanced',
91+
},
8392
],
8493
tools: {
8594
access: ['http_request'],
@@ -90,6 +99,7 @@ Example:
9099
headers: { type: 'json', description: 'Request headers' },
91100
body: { type: 'json', description: 'Request body data' },
92101
params: { type: 'json', description: 'URL query parameters' },
102+
timeout: { type: 'number', description: 'Request timeout in milliseconds' },
93103
},
94104
outputs: {
95105
data: { type: 'json', description: 'API response data (JSON, text, or other formats)' },

apps/sim/lib/core/security/input-validation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ export async function secureFetchWithPinnedIP(
931931
method: options.method || 'GET',
932932
headers: sanitizedHeaders,
933933
agent,
934-
timeout: options.timeout || 30000,
934+
timeout: options.timeout || 300000, // Default 5 minutes
935935
}
936936

937937
const protocol = isHttps ? https : http
@@ -1011,7 +1011,7 @@ export async function secureFetchWithPinnedIP(
10111011

10121012
req.on('timeout', () => {
10131013
req.destroy()
1014-
reject(new Error('Request timeout'))
1014+
reject(new Error(`Request timed out after ${requestOptions.timeout}ms`))
10151015
})
10161016

10171017
if (options.body) {

0 commit comments

Comments
 (0)