Skip to content

Commit e07ed4c

Browse files
fix(icypeas): handle 200 validationErrors body as a BAD_INPUT verdict
1 parent 63a3e6d commit e07ed4c

3 files changed

Lines changed: 102 additions & 1 deletion

File tree

apps/sim/tools/icypeas-hosting.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,72 @@ describe('Icypeas verify-email pricing', () => {
8383
})
8484
})
8585

86+
describe('Icypeas transformResponse validation errors', () => {
87+
it('verify-email maps a 200 validationErrors body to a BAD_INPUT verdict', async () => {
88+
const response = new Response(
89+
JSON.stringify({
90+
success: false,
91+
validationErrors: [
92+
{ expected: 'email', type: 'required', field: 'email', message: 'validation_required' },
93+
],
94+
}),
95+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
96+
)
97+
98+
const result = await icypeasVerifyEmailTool.transformResponse!(response, {
99+
apiKey: 'test-key',
100+
email: 'support@stripe.com',
101+
} as any)
102+
103+
expect(result.success).toBe(true)
104+
expect((result.output as any).status).toBe('BAD_INPUT')
105+
expect((result.output as any).valid).toBe(false)
106+
expect((result.output as any).email).toBe('support@stripe.com')
107+
expect((result.output as any).searchId).toBeNull()
108+
})
109+
110+
it('find-email maps a 200 validationErrors body to a BAD_INPUT verdict', async () => {
111+
const response = new Response(
112+
JSON.stringify({
113+
success: false,
114+
validationErrors: [
115+
{
116+
expected: 'string',
117+
type: 'required',
118+
field: 'domainOrCompany',
119+
message: 'validation_required',
120+
},
121+
],
122+
}),
123+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
124+
)
125+
126+
const result = await icypeasFindEmailTool.transformResponse!(response, {
127+
apiKey: 'test-key',
128+
domainOrCompany: 'stripe.com',
129+
} as any)
130+
131+
expect(result.success).toBe(true)
132+
expect((result.output as any).status).toBe('BAD_INPUT')
133+
expect((result.output as any).email).toBeNull()
134+
expect((result.output as any).searchId).toBeNull()
135+
})
136+
137+
it('verify-email still throws when a success body has no item _id', async () => {
138+
const response = new Response(JSON.stringify({ success: true, item: {} }), {
139+
status: 200,
140+
headers: { 'Content-Type': 'application/json' },
141+
})
142+
143+
await expect(
144+
icypeasVerifyEmailTool.transformResponse!(response, {
145+
apiKey: 'test-key',
146+
email: 'jane@example.com',
147+
} as any)
148+
).rejects.toThrow(/item _id/)
149+
})
150+
})
151+
86152
describe('Icypeas find-email postProcess poll', () => {
87153
it('polls the results endpoint until terminal status and returns the email', async () => {
88154
vi.useFakeTimers()

apps/sim/tools/icypeas/find_email.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ export const icypeasFindEmailTool: ToolConfig<IcypeasFindEmailParams, IcypeasFin
112112
throw new Error(`Icypeas API error: ${response.status} - ${errorText}`)
113113
}
114114
const json = (await response.json()) as Record<string, unknown>
115+
// Icypeas returns HTTP 200 with { success: false, validationErrors: [...] } when
116+
// it rejects the input up front (missing/invalid name or domain). There is no item
117+
// to poll — this is a BAD_INPUT verdict, not a transport error. Surface it as a
118+
// successful run with a null email so the enrichment cascade records the verdict
119+
// instead of inflating the runner's error count.
120+
if (json.success === false || Array.isArray(json.validationErrors)) {
121+
return {
122+
success: true,
123+
output: {
124+
searchId: null,
125+
status: 'BAD_INPUT',
126+
email: null,
127+
firstname: null,
128+
lastname: null,
129+
item: json,
130+
},
131+
}
132+
}
115133
// Submit response: { success: true, item: { _id: '...', status: 'NONE', ... } }
116134
const item = (json.item as Record<string, unknown> | undefined) ?? {}
117135
const searchId = (item._id as string | undefined) ?? null

apps/sim/tools/icypeas/verify_email.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,29 @@ export const icypeasVerifyEmailTool: ToolConfig<
9999
}),
100100
},
101101

102-
transformResponse: async (response: Response) => {
102+
transformResponse: async (response: Response, params?: IcypeasVerifyEmailParams) => {
103103
if (!response.ok) {
104104
const errorText = await response.text()
105105
throw new Error(`Icypeas API error: ${response.status} - ${errorText}`)
106106
}
107107
const json = (await response.json()) as Record<string, unknown>
108+
// Icypeas returns HTTP 200 with { success: false, validationErrors: [...] } when
109+
// it rejects the input up front (bad format, role-based address, etc.). There is
110+
// no item to poll — this is a BAD_INPUT verdict, not a transport error. Surface it
111+
// as a successful run with valid=false so the enrichment cascade records the
112+
// verdict instead of inflating the runner's error count.
113+
if (json.success === false || Array.isArray(json.validationErrors)) {
114+
return {
115+
success: true,
116+
output: {
117+
searchId: null,
118+
status: 'BAD_INPUT',
119+
email: params?.email ?? null,
120+
valid: false,
121+
item: json,
122+
},
123+
}
124+
}
108125
// Submit response: { success: true, item: { _id: '...', status: 'NONE', ... } }
109126
const item = (json.item as Record<string, unknown> | undefined) ?? {}
110127
const searchId = (item._id as string | undefined) ?? null

0 commit comments

Comments
 (0)