diff --git a/apps/sim/tools/jotform/jotform.test.ts b/apps/sim/tools/jotform/jotform.test.ts index 0c19b087f2..3205114604 100644 --- a/apps/sim/tools/jotform/jotform.test.ts +++ b/apps/sim/tools/jotform/jotform.test.ts @@ -420,3 +420,27 @@ describe('label resources', () => { expect(depth).toBeLessThanOrEqual(32) }) }) + +describe('error envelope robustness', () => { + /** + * Jotform quotes `responseCode` on some endpoints and not others. A + * `typeof === 'number'` test silently skips the check on the quoted ones, turning + * an auth failure into a successful tool result with empty output. + */ + it('throws on a quoted non-2xx responseCode', async () => { + await expect( + parseJotformResponse( + jsonResponse({ responseCode: '401', message: 'Invalid API Key' }), + 'Jotform Get Form' + ) + ).rejects.toThrow('Jotform Get Form error (401): Invalid API Key') + }) + + /** An upstream gateway can answer with an HTML page instead of the JSON envelope. */ + it('caps a non-JSON error body instead of inlining the whole page', async () => { + const page = `${'x'.repeat(5000)}` + await expect( + parseJotformResponse(new Response(page, { status: 502 }), 'Jotform Get Form') + ).rejects.toThrow(/^Jotform Get Form error \(502\): .{1,320}$/s) + }) +}) diff --git a/apps/sim/tools/jotform/utils.ts b/apps/sim/tools/jotform/utils.ts index 82b38beb6c..6cc57bdc1b 100644 --- a/apps/sim/tools/jotform/utils.ts +++ b/apps/sim/tools/jotform/utils.ts @@ -1,4 +1,5 @@ import { getErrorMessage } from '@sim/utils/errors' +import { truncate } from '@sim/utils/string' /** * Jotform serves the same REST surface from three regional hosts, and an API key @@ -75,11 +76,17 @@ export async function parseJotformResponse( } const message = typeof data?.message === 'string' ? data.message : null - const responseCode = typeof data?.responseCode === 'number' ? data.responseCode : null + /* Jotform types `responseCode` inconsistently across endpoints, quoting it on some, + so a `typeof === 'number'` test would skip the check on the quoted ones. */ + const responseCode = toNumberOrNull(data?.responseCode) if (!response.ok || (responseCode !== null && (responseCode < 200 || responseCode >= 300))) { const status = responseCode ?? response.status - throw new Error(`${label} error (${status}): ${message || text || response.statusText}`) + /* An error body is not always the documented JSON envelope — an upstream gateway + can return an HTML page — so the raw fallback is capped before it becomes the + error message. */ + const detail = message || truncate(text, 300) || response.statusText + throw new Error(`${label} error (${status}): ${detail}`) } const rawResultSet = data?.resultSet as Record | undefined