FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(enrow): return a failed tool response when the poll fails · simstudioai/sim@f952aaa · GitHub

Commit f952aaa

Browse files
Waleed Latif
committed
fix(enrow): return a failed tool response when the poll fails
A throw out of `postProcess` never reaches the user. `executeTool` wraps every `postProcess` call in a catch that logs and then restores the pre-`postProcess` result — which for these tools is the *submit* response: `success: true` with every result field null. So a poll that timed out, exhausted its retries, or hit a terminal status was reported as a successful lookup that simply found nothing. Both Enrow tools now catch the poll failure and return it as `success: false` with the message and the job id preserved. Returning rather than throwing also stops the hosted-key cost hook, which is gated on `finalResult.success`, from billing an execution that produced no result. The eleven existing failure-path tests asserted the throwing contract and were bypassing the executor wrapper, which is exactly why this went unnoticed; they now assert the returned failure. One test pins the whole shape — `success: false`, the error, and the null-field output — so the silent-success regression cannot come back. Also corrects the rate-limit comment again: `executeTool` retries an upstream 429/503 and can re-acquire a key, so an admitted execution can issue more than one POST. The 60/min figure is an admission cap, and even at that retry ceiling it stays an order of magnitude under Enrow's documented 600/min.
1 parent 4064f91 commit f952aaa

4 files changed

Lines changed: 185 additions & 50 deletions

File tree

‎apps/sim/tools/enrow/find_email.test.ts‎

Lines changed: 111 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,14 @@ describe('enrow_find_email', () => {
158158
.mockResolvedValueOnce(jsonResponse(401, { message: 'invalid api key' }))
159159
vi.stubGlobal('fetch', fetchMock)
160160

161-
await expect(
162-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
163-
).rejects.toThrow(/poll error: 401 - .*invalid api key/)
161+
const failed = await enrowFindEmailTool.postProcess!(
162+
submittedFindResult,
163+
findParams,
164+
executeTool
165+
)
166+
167+
expect(failed.success).toBe(false)
168+
expect(failed.error).toMatch(/poll error: 401 - .*invalid api key/)
164169

165170
expect(fetchMock).toHaveBeenCalledTimes(2)
166171
})
@@ -250,9 +255,14 @@ describe('enrow_find_email', () => {
250255
.mockRejectedValue(new DOMException('The operation timed out.', 'TimeoutError'))
251256
vi.stubGlobal('fetch', fetchMock)
252257

253-
await expect(
254-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
255-
).rejects.toThrow('Enrow find-email did not complete within the polling window')
258+
const failed = await enrowFindEmailTool.postProcess!(
259+
submittedFindResult,
260+
findParams,
261+
executeTool
262+
)
263+
264+
expect(failed.success).toBe(false)
265+
expect(failed.error).toContain('Enrow find-email did not complete within the polling window')
256266

257267
expect(fetchMock).toHaveBeenCalledTimes(1)
258268
})
@@ -261,9 +271,14 @@ describe('enrow_find_email', () => {
261271
const fetchMock = vi.fn().mockRejectedValue(new TypeError('fetch failed'))
262272
vi.stubGlobal('fetch', fetchMock)
263273

264-
await expect(
265-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
266-
).rejects.toThrow('fetch failed')
274+
const failed = await enrowFindEmailTool.postProcess!(
275+
submittedFindResult,
276+
findParams,
277+
executeTool
278+
)
279+
280+
expect(failed.success).toBe(false)
281+
expect(failed.error).toContain('fetch failed')
267282
})
268283

269284
it('never sleeps past the wall-clock deadline when the polls themselves are slow', async () => {
@@ -286,9 +301,14 @@ describe('enrow_find_email', () => {
286301
})
287302
vi.stubGlobal('fetch', fetchMock)
288303

289-
await expect(
290-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
291-
).rejects.toThrow('Enrow find-email did not complete within the polling window')
304+
const failed = await enrowFindEmailTool.postProcess!(
305+
submittedFindResult,
306+
findParams,
307+
executeTool
308+
)
309+
310+
expect(failed.success).toBe(false)
311+
expect(failed.error).toContain('Enrow find-email did not complete within the polling window')
292312

293313
// Second poll ends past the deadline, so its 15s backoff must be dropped to
294314
// zero rather than clamped against the 99s that `elapsed` still thinks it has.
@@ -308,9 +328,14 @@ describe('enrow_find_email', () => {
308328
})
309329
vi.stubGlobal('fetch', fetchMock)
310330

311-
await expect(
312-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
313-
).rejects.toThrow('Enrow find-email did not complete within the polling window')
331+
const failed = await enrowFindEmailTool.postProcess!(
332+
submittedFindResult,
333+
findParams,
334+
executeTool
335+
)
336+
337+
expect(failed.success).toBe(false)
338+
expect(failed.error).toContain('Enrow find-email did not complete within the polling window')
314339

315340
expect(fetchMock).toHaveBeenCalledTimes(39)
316341
const total = sleepDelays().reduce((sum, delay) => sum + delay, 0)
@@ -322,9 +347,14 @@ describe('enrow_find_email', () => {
322347
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(500, { message: 'boom' }))
323348
vi.stubGlobal('fetch', fetchMock)
324349

325-
await expect(
326-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
327-
).rejects.toThrow(/poll error: 500 - .*boom/)
350+
const failed = await enrowFindEmailTool.postProcess!(
351+
submittedFindResult,
352+
findParams,
353+
executeTool
354+
)
355+
356+
expect(failed.success).toBe(false)
357+
expect(failed.error).toMatch(/poll error: 500 - .*boom/)
328358

329359
expect(fetchMock).toHaveBeenCalledTimes(MAX_TRANSIENT_RETRIES + 1)
330360
})
@@ -333,9 +363,14 @@ describe('enrow_find_email', () => {
333363
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(403, { message: 'forbidden' }))
334364
vi.stubGlobal('fetch', fetchMock)
335365

336-
await expect(
337-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
338-
).rejects.toThrow(/poll error: 403 - .*forbidden/)
366+
const failed = await enrowFindEmailTool.postProcess!(
367+
submittedFindResult,
368+
findParams,
369+
executeTool
370+
)
371+
372+
expect(failed.success).toBe(false)
373+
expect(failed.error).toMatch(/poll error: 403 - .*forbidden/)
339374

340375
expect(fetchMock).toHaveBeenCalledTimes(1)
341376
})
@@ -344,9 +379,14 @@ describe('enrow_find_email', () => {
344379
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(600, { message: 'nonsense' }))
345380
vi.stubGlobal('fetch', fetchMock)
346381

347-
await expect(
348-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
349-
).rejects.toThrow(/poll error: 600 - .*nonsense/)
382+
const failed = await enrowFindEmailTool.postProcess!(
383+
submittedFindResult,
384+
findParams,
385+
executeTool
386+
)
387+
388+
expect(failed.success).toBe(false)
389+
expect(failed.error).toMatch(/poll error: 600 - .*nonsense/)
350390

351391
expect(fetchMock).toHaveBeenCalledTimes(1)
352392
})
@@ -360,9 +400,14 @@ describe('enrow_find_email', () => {
360400
const fetchMock = vi.fn().mockResolvedValue(stalled)
361401
vi.stubGlobal('fetch', fetchMock)
362402

363-
await expect(
364-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
365-
).rejects.toThrow('Enrow find-email did not complete within the polling window')
403+
const failed = await enrowFindEmailTool.postProcess!(
404+
submittedFindResult,
405+
findParams,
406+
executeTool
407+
)
408+
409+
expect(failed.success).toBe(false)
410+
expect(failed.error).toContain('Enrow find-email did not complete within the polling window')
366411
})
367412

368413
it('keeps the status when an error body cannot be read', async () => {
@@ -373,18 +418,51 @@ describe('enrow_find_email', () => {
373418
const fetchMock = vi.fn().mockResolvedValue(unreadable)
374419
vi.stubGlobal('fetch', fetchMock)
375420

376-
await expect(
377-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
378-
).rejects.toThrow('Enrow find-email poll error: 403 - <unreadable body>')
421+
const failed = await enrowFindEmailTool.postProcess!(
422+
submittedFindResult,
423+
findParams,
424+
executeTool
425+
)
426+
427+
expect(failed.success).toBe(false)
428+
expect(failed.error).toContain('Enrow find-email poll error: 403 - <unreadable body>')
429+
})
430+
431+
it('reports a failed poll as a failed tool response, not a null-field success', async () => {
432+
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(403, { message: 'forbidden' }))
433+
vi.stubGlobal('fetch', fetchMock)
434+
435+
const failed = await enrowFindEmailTool.postProcess!(
436+
submittedFindResult,
437+
findParams,
438+
executeTool
439+
)
440+
441+
/*
442+
* `executeTool` swallows a throw from `postProcess` and restores the submit
443+
* response — `success: true` with every field null — so the failure has to
444+
* be returned rather than thrown or the user sees a successful lookup that
445+
* merely found nothing.
446+
*/
447+
expect(failed.success).toBe(false)
448+
expect(failed.error).toMatch(/poll error: 403/)
449+
expect(failed.output.id).toBe(submittedFindResult.output.id)
450+
expect(failed.output.email).toBeNull()
451+
expect(failed.output.qualification).toBeNull()
379452
})
380453

381454
it('gives up after the polling window when every poll stays 202', async () => {
382455
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(202, { qualification: 'ongoing' }))
383456
vi.stubGlobal('fetch', fetchMock)
384457

385-
await expect(
386-
enrowFindEmailTool.postProcess!(submittedFindResult, findParams, executeTool)
387-
).rejects.toThrow('Enrow find-email did not complete within the polling window')
458+
const failed = await enrowFindEmailTool.postProcess!(
459+
submittedFindResult,
460+
findParams,
461+
executeTool
462+
)
463+
464+
expect(failed.success).toBe(false)
465+
expect(failed.error).toContain('Enrow find-email did not complete within the polling window')
388466

389467
expect(fetchMock).toHaveBeenCalledTimes(MAX_POLLS)
390468
})

‎apps/sim/tools/enrow/find_email.ts‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
12
import { enrowHosting } from '@/tools/enrow/hosting'
23
import { pollEnrowJob } from '@/tools/enrow/poll'
34
import type {
@@ -134,12 +135,41 @@ export const enrowFindEmailTool: ToolConfig<EnrowFindEmailParams, EnrowFindEmail
134135
throw new Error('Enrow find-email did not return a job id to poll')
135136
}
136137

137-
const data = await pollEnrowJob({
138-
resultUrl: 'https://api.enrow.io/email/find/single',
139-
jobId,
140-
apiKey: params.apiKey,
141-
label: 'Enrow find-email',
142-
})
138+
/*
139+
* A failed poll must surface as a failed tool response, not as a throw.
140+
*
141+
* `executeTool` wraps every `postProcess` call in a catch that logs and
142+
* then restores the pre-`postProcess` result. That result is the *submit*
143+
* response — `success: true` with every result field null — so an escaping
144+
* error would be reported to the user as a successful lookup that simply
145+
* found nothing. Returning the failure explicitly keeps the reason
146+
* attached, and `success: false` also stops the hosted-key cost hook from
147+
* billing an execution that produced no result.
148+
*/
149+
let data: Record<string, unknown>
150+
try {
151+
data = await pollEnrowJob({
152+
resultUrl: 'https://api.enrow.io/email/find/single',
153+
jobId,
154+
apiKey: params.apiKey,
155+
label: 'Enrow find-email',
156+
})
157+
} catch (error) {
158+
return {
159+
success: false,
160+
error: getErrorMessage(error, 'Enrow find-email polling failed'),
161+
output: {
162+
id: jobId,
163+
email: null,
164+
qualification: null,
165+
fullname: null,
166+
firstname: null,
167+
lastname: null,
168+
company_name: null,
169+
company_domain: null,
170+
},
171+
}
172+
}
143173

144174
return {
145175
success: true,

‎apps/sim/tools/enrow/hosting.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ export function enrowHosting<P>(
5151
* call, so the poll loop's own GETs are not counted against it; nothing
5252
* here throttles a single call's polling.
5353
*
54-
* That is the right shape for what Enrow actually publishes. Its
54+
* It is still the right order of magnitude for what Enrow publishes. Its
5555
* documented limit is 10 req/s per API key on every *POST* endpoint —
56-
* 600/min (https://docs.enrow.io/rate-limits). Each execution issues
57-
* exactly one POST, so 60/min sits an order of magnitude under the
58-
* documented ceiling. The polling GETs are not covered by that limit at
59-
* all, and each one is separated by a 3s interval within a 120s budget.
56+
* 600/min (https://docs.enrow.io/rate-limits). An admitted execution
57+
* issues at least one POST and, when `executeTool` retries an upstream
58+
* 429/503 and re-acquires a key, up to a small handful; even at that
59+
* ceiling 60 admissions/min stays an order of magnitude under 600. The
60+
* polling GETs are not covered by the documented POST limit at all, and
61+
* within one call they are serialized 3s apart under a 120s budget.
6062
*/
6163
requestsPerMinute: 60,
6264
},

‎apps/sim/tools/enrow/verify_email.ts‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
12
import { enrowHosting } from '@/tools/enrow/hosting'
23
import { pollEnrowJob } from '@/tools/enrow/poll'
34
import type {
@@ -104,12 +105,36 @@ export const enrowVerifyEmailTool: ToolConfig<EnrowVerifyEmailParams, EnrowVerif
104105
throw new Error('Enrow verify-email did not return a job id to poll')
105106
}
106107

107-
const data = await pollEnrowJob({
108-
resultUrl: 'https://api.enrow.io/email/verify/single',
109-
jobId,
110-
apiKey: params.apiKey,
111-
label: 'Enrow verify-email',
112-
})
108+
/*
109+
* A failed poll must surface as a failed tool response, not as a throw.
110+
*
111+
* `executeTool` wraps every `postProcess` call in a catch that logs and
112+
* then restores the pre-`postProcess` result. That result is the *submit*
113+
* response — `success: true` with every result field null — so an escaping
114+
* error would be reported to the user as a successful lookup that simply
115+
* found nothing. Returning the failure explicitly keeps the reason
116+
* attached, and `success: false` also stops the hosted-key cost hook from
117+
* billing an execution that produced no result.
118+
*/
119+
let data: Record<string, unknown>
120+
try {
121+
data = await pollEnrowJob({
122+
resultUrl: 'https://api.enrow.io/email/verify/single',
123+
jobId,
124+
apiKey: params.apiKey,
125+
label: 'Enrow verify-email',
126+
})
127+
} catch (error) {
128+
return {
129+
success: false,
130+
error: getErrorMessage(error, 'Enrow verify-email polling failed'),
131+
output: {
132+
id: jobId,
133+
email: null,
134+
qualification: null,
135+
},
136+
}
137+
}
113138

114139
return {
115140
success: true,

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL