diff --git a/apps/sim/tools/icypeas-hosting.test.ts b/apps/sim/tools/icypeas-hosting.test.ts index 288f2e773f8..554ccbb9c67 100644 --- a/apps/sim/tools/icypeas-hosting.test.ts +++ b/apps/sim/tools/icypeas-hosting.test.ts @@ -83,6 +83,73 @@ describe('Icypeas verify-email pricing', () => { }) }) +describe('Icypeas transformResponse validation errors', () => { + it('verify-email throws the human-readable reason on a 200 validationErrors body', async () => { + const response = new Response( + JSON.stringify({ + success: false, + validationErrors: [ + { + field: 'type', + message: 'insufficient_credits', + humanReadableMessage: 'Insufficient credits to run the search', + type: 'InsufficientCredits', + }, + ], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + + await expect(icypeasVerifyEmailTool.transformResponse!(response)).rejects.toThrow( + /Insufficient credits to run the search/ + ) + }) + + it('find-email throws the human-readable reason on a 200 validationErrors body', async () => { + const response = new Response( + JSON.stringify({ + success: false, + validationErrors: [ + { + field: 'type', + message: 'insufficient_credits', + humanReadableMessage: 'Insufficient credits to run the search', + type: 'InsufficientCredits', + }, + ], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + + await expect(icypeasFindEmailTool.transformResponse!(response)).rejects.toThrow( + /Insufficient credits to run the search/ + ) + }) + + it('verify-email falls back to message when humanReadableMessage is absent', async () => { + const response = new Response( + JSON.stringify({ + success: false, + validationErrors: [{ field: 'email', message: 'validation_required' }], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + + await expect(icypeasVerifyEmailTool.transformResponse!(response)).rejects.toThrow( + /validation_required/ + ) + }) + + it('verify-email still throws when a success body has no item _id', async () => { + const response = new Response(JSON.stringify({ success: true, item: {} }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + + await expect(icypeasVerifyEmailTool.transformResponse!(response)).rejects.toThrow(/item _id/) + }) +}) + describe('Icypeas find-email postProcess poll', () => { it('polls the results endpoint until terminal status and returns the email', async () => { vi.useFakeTimers() diff --git a/apps/sim/tools/icypeas/find_email.ts b/apps/sim/tools/icypeas/find_email.ts index 89fe64ab83e..ffc69c6c0f7 100644 --- a/apps/sim/tools/icypeas/find_email.ts +++ b/apps/sim/tools/icypeas/find_email.ts @@ -112,6 +112,19 @@ export const icypeasFindEmailTool: ToolConfig + // Icypeas signals submit-time failures as HTTP 200 { success: false, validationErrors: [...] } + // — malformed input, insufficient credits, rate limits, etc. None of these are a + // search verdict (those only come from polling), so fail fast with the human-readable + // reason rather than a cryptic missing-_id error or a fabricated empty result. + const validationErrors = json.validationErrors + if (Array.isArray(validationErrors) && validationErrors.length > 0) { + const first = validationErrors[0] as Record | undefined + const reason = + (first?.humanReadableMessage as string | undefined) ?? + (first?.message as string | undefined) ?? + 'validation error' + throw new Error(`Icypeas email-search rejected the request: ${reason}`) + } // Submit response: { success: true, item: { _id: '...', status: 'NONE', ... } } const item = (json.item as Record | undefined) ?? {} const searchId = (item._id as string | undefined) ?? null diff --git a/apps/sim/tools/icypeas/verify_email.ts b/apps/sim/tools/icypeas/verify_email.ts index 827a81fc766..9b38381cf9b 100644 --- a/apps/sim/tools/icypeas/verify_email.ts +++ b/apps/sim/tools/icypeas/verify_email.ts @@ -105,6 +105,20 @@ export const icypeasVerifyEmailTool: ToolConfig< throw new Error(`Icypeas API error: ${response.status} - ${errorText}`) } const json = (await response.json()) as Record + // Icypeas signals submit-time failures as HTTP 200 { success: false, validationErrors: [...] } + // — malformed input, insufficient credits, rate limits, etc. None of these are a + // deliverability verdict (those only come from polling), so fail fast with the + // human-readable reason rather than a cryptic missing-_id error or a fabricated + // valid=false result. + const validationErrors = json.validationErrors + if (Array.isArray(validationErrors) && validationErrors.length > 0) { + const first = validationErrors[0] as Record | undefined + const reason = + (first?.humanReadableMessage as string | undefined) ?? + (first?.message as string | undefined) ?? + 'validation error' + throw new Error(`Icypeas email-verification rejected the request: ${reason}`) + } // Submit response: { success: true, item: { _id: '...', status: 'NONE', ... } } const item = (json.item as Record | undefined) ?? {} const searchId = (item._id as string | undefined) ?? null