From 3db16173db7f75f4f37c702eb73168c2a0730fae Mon Sep 17 00:00:00 2001 From: Ryan Smith Date: Fri, 29 May 2026 01:15:24 -0600 Subject: [PATCH 1/2] fix(executor): harden JSON parsing and error visibility in block handlers Replace bare JSON.parse() calls across 5 executor handler files with the project's own parseJSON/parseJSONOrThrow utilities from executor/utils/json.ts. Add logger.warn to 2 empty catch blocks in the router handler that previously swallowed errors silently. The utilities already exist and are imported by some of these files (for parseObjectStrings). This makes handlers consistent with the project's established parsing patterns. Also adds test coverage for error paths: 2 new test files (response-handler, human-in-the-loop-handler) and error-path test cases for 3 existing test files (router, api, generic handlers). --- .../executor/handlers/api/api-handler.test.ts | 15 ++ apps/sim/executor/handlers/api/api-handler.ts | 11 +- .../handlers/generic/generic-handler.test.ts | 35 ++++ .../handlers/generic/generic-handler.ts | 10 +- .../human-in-the-loop-handler.test.ts | 176 ++++++++++++++++++ .../human-in-the-loop-handler.ts | 12 +- .../response/response-handler.test.ts | 139 ++++++++++++++ .../handlers/response/response-handler.ts | 12 +- .../handlers/router/router-handler.test.ts | 50 +++++ .../handlers/router/router-handler.ts | 28 +-- 10 files changed, 445 insertions(+), 43 deletions(-) create mode 100644 apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.test.ts create mode 100644 apps/sim/executor/handlers/response/response-handler.test.ts diff --git a/apps/sim/executor/handlers/api/api-handler.test.ts b/apps/sim/executor/handlers/api/api-handler.test.ts index e2eeca7f795..826b226f1d8 100644 --- a/apps/sim/executor/handlers/api/api-handler.test.ts +++ b/apps/sim/executor/handlers/api/api-handler.test.ts @@ -195,6 +195,21 @@ describe('ApiBlockHandler', () => { ) }) + it('should handle malformed JSON body by keeping original string', async () => { + const inputs = { + url: 'https://example.com/api', + body: '{invalid json body', + } + + await handler.execute(mockContext, mockBlock, inputs) + + expect(mockExecuteTool).toHaveBeenCalledWith( + 'http_request', + expect.objectContaining({ body: '{invalid json body' }), + { executionContext: mockContext } + ) + }) + it('should handle null body by converting to undefined', async () => { const inputs = { url: 'https://example.com/api', diff --git a/apps/sim/executor/handlers/api/api-handler.ts b/apps/sim/executor/handlers/api/api-handler.ts index b6a28517d7d..f5c1b4ac543 100644 --- a/apps/sim/executor/handlers/api/api-handler.ts +++ b/apps/sim/executor/handlers/api/api-handler.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import { BlockType, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' +import { parseJSON } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import { getTool } from '@/tools/utils' @@ -53,12 +54,10 @@ export class ApiBlockHandler implements BlockHandler { if (processedInputs.body !== undefined) { if (typeof processedInputs.body === 'string') { - try { - const trimmedBody = processedInputs.body.trim() - if (trimmedBody.startsWith('{') || trimmedBody.startsWith('[')) { - processedInputs.body = JSON.parse(trimmedBody) - } - } catch (e) {} + const trimmedBody = processedInputs.body.trim() + if (trimmedBody.startsWith('{') || trimmedBody.startsWith('[')) { + processedInputs.body = parseJSON(trimmedBody, processedInputs.body) + } } else if (processedInputs.body === null) { processedInputs.body = undefined } diff --git a/apps/sim/executor/handlers/generic/generic-handler.test.ts b/apps/sim/executor/handlers/generic/generic-handler.test.ts index be3baec9aed..2bf51886233 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.test.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.test.ts @@ -1,6 +1,7 @@ import '@sim/testing/mocks/executor' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { getBlock } from '@/blocks/index' import { BlockType } from '@/executor/constants' import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' import type { ExecutionContext } from '@/executor/types' @@ -144,4 +145,38 @@ describe('GenericBlockHandler', () => { 'Block execution of Some Custom Tool failed with no error message' ) }) + + it('should handle malformed json field input by keeping original string', async () => { + const mockGetBlock = vi.mocked(getBlock) + mockGetBlock.mockReturnValue({ + type: 'custom-type', + name: 'Custom Block', + description: 'Test block', + category: 'tools', + bgColor: '#000', + icon: () => null, + subBlocks: [], + tools: { + access: ['some_custom_tool'], + config: { + tool: () => 'some_custom_tool', + params: (p: Record) => p, + }, + }, + inputs: { + data: { type: 'json' }, + }, + outputs: {}, + } as unknown as ReturnType) + + const inputs = { data: '{not valid json' } + + await handler.execute(mockContext, mockBlock, inputs) + + expect(mockExecuteTool).toHaveBeenCalledWith( + 'some_custom_tool', + expect.objectContaining({ data: '{not valid json' }), + { executionContext: mockContext } + ) + }) }) diff --git a/apps/sim/executor/handlers/generic/generic-handler.ts b/apps/sim/executor/handlers/generic/generic-handler.ts index c6ee89dd633..f07bf2c3372 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.ts @@ -1,8 +1,8 @@ import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' import { getBlock } from '@/blocks/index' import { isMcpTool } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' +import { parseJSON } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import { getTool } from '@/tools/utils' @@ -45,13 +45,7 @@ export class GenericBlockHandler implements BlockHandler { if (typeof value === 'string' && value.trim().length > 0) { const inputType = typeof inputSchema === 'object' ? inputSchema.type : inputSchema if (inputType === 'json' || inputType === 'array') { - try { - finalInputs[key] = JSON.parse(value.trim()) - } catch (error) { - logger.warn(`Failed to parse ${inputType} field "${key}":`, { - error: toError(error).message, - }) - } + finalInputs[key] = parseJSON(value, value) } } } diff --git a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.test.ts b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.test.ts new file mode 100644 index 00000000000..e0c22b6474f --- /dev/null +++ b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.test.ts @@ -0,0 +1,176 @@ +import '@sim/testing/mocks/executor' + +import { urlsMock, urlsMockFns } from '@sim/testing' +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { BlockType } from '@/executor/constants' +import { HumanInTheLoopBlockHandler } from '@/executor/handlers/human-in-the-loop/human-in-the-loop-handler' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' +import { executeTool } from '@/tools' + +vi.mock('@/lib/core/utils/urls', () => urlsMock) + +const { mockGeneratePauseContextId, mockMapNodeMetadataToPauseScopes } = vi.hoisted(() => ({ + mockGeneratePauseContextId: vi.fn(() => 'test-pause-context-id'), + mockMapNodeMetadataToPauseScopes: vi.fn(() => ({ + parallelScope: undefined, + loopScope: undefined, + })), +})) + +vi.mock('@/executor/human-in-the-loop/utils', () => ({ + generatePauseContextId: mockGeneratePauseContextId, + mapNodeMetadataToPauseScopes: mockMapNodeMetadataToPauseScopes, +})) + +vi.mock('@/executor/utils/builder-data', () => ({ + convertBuilderDataToJson: vi.fn(() => ({ key: 'value' })), + convertPropertyValue: vi.fn((prop: any) => prop.value), +})) + +vi.mock('@/executor/utils/block-data', () => ({ + collectBlockData: vi.fn(() => ({ + blockData: {}, + blockNameMapping: {}, + })), +})) + +const mockExecuteTool = executeTool as Mock + +describe('HumanInTheLoopBlockHandler', () => { + let handler: HumanInTheLoopBlockHandler + let mockBlock: SerializedBlock + let mockContext: ExecutionContext + + beforeEach(() => { + vi.clearAllMocks() + + handler = new HumanInTheLoopBlockHandler() + + mockBlock = { + id: 'hitl-block-1', + metadata: { id: BlockType.HUMAN_IN_THE_LOOP, name: 'Test HITL Block' }, + position: { x: 0, y: 0 }, + config: { tool: BlockType.HUMAN_IN_THE_LOOP, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + mockContext = { + workflowId: 'test-workflow-id', + blockStates: new Map(), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopExecutions: new Map(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + completedLoops: new Set(), + } + + urlsMockFns.mockGetBaseUrl.mockReturnValue('http://localhost:3000') + mockExecuteTool.mockResolvedValue({ success: true, output: {} }) + mockGeneratePauseContextId.mockReturnValue('test-pause-context-id') + mockMapNodeMetadataToPauseScopes.mockReturnValue({ + parallelScope: undefined, + loopScope: undefined, + }) + }) + + it('should return true for human-in-the-loop blocks', () => { + expect(handler.canHandle(mockBlock)).toBe(true) + }) + + it('should return false for non-hitl blocks', () => { + const nonHitlBlock: SerializedBlock = { + ...mockBlock, + metadata: { id: 'other-block' }, + } + expect(handler.canHandle(nonHitlBlock)).toBe(false) + }) + + it('should execute with human operation and return correct response shape', async () => { + const inputs = { + operation: 'human', + inputFormat: [{ id: 'field-1', name: 'username', label: 'Username', type: 'string' }], + builderData: [{ id: '1', name: 'result', type: 'string', value: 'test' }], + } + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.response).toBeDefined() + expect(result.response.status).toBe(200) + expect(result.response.headers).toHaveProperty('Content-Type') + expect(result.response.data).toHaveProperty('operation', 'human') + expect(result.response.data).toHaveProperty('responseStructure') + expect(result.response.data).toHaveProperty('inputFormat') + expect(result.response.data).toHaveProperty('submission', null) + expect(result._pauseMetadata).toBeDefined() + expect(result._pauseMetadata.pauseKind).toBe('human') + }) + + it('should handle malformed JSON data in api operation mode', async () => { + const inputs = { + operation: 'api', + dataMode: 'json', + data: '{invalid json}', + } + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result).toBeDefined() + expect(result.response).toBeDefined() + expect(result.response.data).toBe('{invalid json}') + }) + + it('should handle valid JSON data in api operation mode', async () => { + const inputs = { + operation: 'api', + dataMode: 'json', + data: '{"message":"hello"}', + } + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.response.data).toMatchObject({ message: 'hello' }) + }) + + it('should return error response on execution failure', async () => { + const inputs = { + operation: 'human', + inputFormat: 'not-an-array', + builderData: 'not-an-array', + } + + mockMapNodeMetadataToPauseScopes.mockImplementation(() => { + throw new Error('Metadata mapping failed') + }) + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.response).toBeDefined() + expect(result.response.status).toBe(500) + expect(result.response.data).toHaveProperty('error') + expect(result.response.data.message).toBe('Metadata mapping failed') + }) + + it('should include resume links when executionId and workflowId exist', async () => { + const contextWithExecution: ExecutionContext = { + ...mockContext, + executionId: 'exec-123', + } + + const inputs = { + operation: 'human', + inputFormat: [], + } + + const result = await handler.execute(contextWithExecution, mockBlock, inputs) + + expect(result.response.data._resume).toBeDefined() + expect(result.url).toBeDefined() + expect(result.resumeEndpoint).toBeDefined() + }) +}) diff --git a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts index 032640d1b7e..5bc6938f24f 100644 --- a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts +++ b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts @@ -17,7 +17,7 @@ import { import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' import { convertBuilderDataToJson, convertPropertyValue } from '@/executor/utils/builder-data' -import { parseObjectStrings } from '@/executor/utils/json' +import { parseJSON, parseObjectStrings } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -253,13 +253,9 @@ export class HumanInTheLoopBlockHandler implements BlockHandler { if (dataMode === 'json' && inputs.data) { if (typeof inputs.data === 'string') { - try { - return JSON.parse(inputs.data) - } catch (error) { - logger.warn('Failed to parse JSON data, returning as string:', error) - return inputs.data - } - } else if (typeof inputs.data === 'object' && inputs.data !== null) { + return parseJSON(inputs.data, inputs.data) + } + if (typeof inputs.data === 'object' && inputs.data !== null) { return inputs.data } return inputs.data diff --git a/apps/sim/executor/handlers/response/response-handler.test.ts b/apps/sim/executor/handlers/response/response-handler.test.ts new file mode 100644 index 00000000000..493bd07bf8d --- /dev/null +++ b/apps/sim/executor/handlers/response/response-handler.test.ts @@ -0,0 +1,139 @@ +import '@sim/testing/mocks/executor' + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { BlockType, HTTP } from '@/executor/constants' +import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' + +vi.mock('@/executor/utils/builder-data', () => ({ + convertBuilderDataToJson: vi.fn(() => ({ key: 'value' })), + convertBuilderDataToJsonString: vi.fn(() => '{"key":"value"}'), +})) + +describe('ResponseBlockHandler', () => { + let handler: ResponseBlockHandler + let mockBlock: SerializedBlock + let mockContext: ExecutionContext + + beforeEach(() => { + vi.clearAllMocks() + + handler = new ResponseBlockHandler() + + mockBlock = { + id: 'response-block-1', + metadata: { id: BlockType.RESPONSE, name: 'Test Response Block' }, + position: { x: 0, y: 0 }, + config: { tool: BlockType.RESPONSE, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + mockContext = { + workflowId: 'test-workflow-id', + blockStates: new Map(), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopExecutions: new Map(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + completedLoops: new Set(), + } + }) + + it('should return true for response blocks', () => { + expect(handler.canHandle(mockBlock)).toBe(true) + }) + + it('should return false for non-response blocks', () => { + const nonResponseBlock: SerializedBlock = { + ...mockBlock, + metadata: { id: 'other-block' }, + } + expect(handler.canHandle(nonResponseBlock)).toBe(false) + }) + + it('should execute with structured data and return correct shape', async () => { + const inputs = { + dataMode: 'structured', + builderData: [{ id: '1', name: 'key', type: 'string', value: 'value' }], + status: '200', + } + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.status).toBe(200) + expect(result.data).toBeDefined() + expect(result.headers).toHaveProperty('Content-Type') + }) + + it('should execute with json data mode and valid JSON string', async () => { + const inputs = { + dataMode: 'json', + data: '{"message":"hello"}', + } + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.status).toBe(200) + expect(result.data).toEqual({ message: 'hello' }) + }) + + it('should execute with json data mode and object input', async () => { + const inputs = { + dataMode: 'json', + data: { message: 'hello' }, + } + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.data).toEqual({ message: 'hello' }) + }) + + it('should handle malformed JSON data by returning original string', async () => { + const inputs = { + dataMode: 'json', + data: '{invalid json}', + } + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.status).toBe(200) + expect(result.data).toBe('{invalid json}') + }) + + it('should return default status 200 when no status provided', async () => { + const inputs = { dataMode: 'json', data: '{}' } + const result = await handler.execute(mockContext, mockBlock, inputs) + expect(result.status).toBe(HTTP.STATUS.OK) + }) + + it('should return default empty object when no data provided', async () => { + const inputs = {} + const result = await handler.execute(mockContext, mockBlock, inputs) + expect(result.data).toEqual({}) + }) + + it('should return error response object on execution failure', async () => { + const inputs = { + dataMode: 'structured', + builderData: [{ id: '1', name: 'key', type: 'string', value: 'value' }], + status: '200', + } + + const { convertBuilderDataToJson } = await import('@/executor/utils/builder-data') + vi.mocked(convertBuilderDataToJson).mockImplementation(() => { + throw new Error('Builder data conversion failed') + }) + + const result = await handler.execute(mockContext, mockBlock, inputs) + + expect(result.status).toBe(500) + expect(result.data).toHaveProperty('error') + expect(result.data.message).toBe('Builder data conversion failed') + }) +}) diff --git a/apps/sim/executor/handlers/response/response-handler.ts b/apps/sim/executor/handlers/response/response-handler.ts index 389ddda76b1..0af93643eba 100644 --- a/apps/sim/executor/handlers/response/response-handler.ts +++ b/apps/sim/executor/handlers/response/response-handler.ts @@ -5,7 +5,7 @@ import { convertBuilderDataToJson, convertBuilderDataToJsonString, } from '@/executor/utils/builder-data' -import { parseObjectStrings } from '@/executor/utils/json' +import { parseJSON, parseObjectStrings } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' const logger = createLogger('ResponseBlockHandler') @@ -56,13 +56,9 @@ export class ResponseBlockHandler implements BlockHandler { if (dataMode === 'json' && inputs.data) { if (typeof inputs.data === 'string') { - try { - return JSON.parse(inputs.data) - } catch (error) { - logger.warn('Failed to parse JSON data, returning as string:', error) - return inputs.data - } - } else if (typeof inputs.data === 'object' && inputs.data !== null) { + return parseJSON(inputs.data, inputs.data) + } + if (typeof inputs.data === 'object' && inputs.data !== null) { return inputs.data } return inputs.data diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index 90e326c2785..c053534385e 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -232,6 +232,22 @@ describe('RouterBlockHandler', () => { }) }) + it('should handle non-JSON error response from provider', async () => { + const inputs = { prompt: 'Test error handling.', apiKey: 'test-api-key' } + + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: false, + status: 502, + json: () => Promise.reject(new Error('Invalid JSON')), + }) + }) + + await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow( + 'Provider API request failed with status 502' + ) + }) + it('should handle server error responses', async () => { const inputs = { prompt: 'Test error handling.', apiKey: 'test-api-key' } @@ -572,6 +588,40 @@ describe('RouterBlockHandler V2', () => { ) }) + it('should handle non-JSON error response from provider in V2', async () => { + const inputs = { + context: 'Test context', + model: 'gpt-4o', + apiKey: 'test-api-key', + routes: JSON.stringify([{ id: 'route-1', title: 'Route 1', value: 'Description' }]), + } + + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: false, + status: 503, + json: () => Promise.reject(new Error('Not JSON')), + }) + }) + + await expect(handler.execute(mockContext, mockRouterV2Block, inputs)).rejects.toThrow( + 'Provider API request failed with status 503' + ) + }) + + it('should throw when routes input is invalid JSON string', async () => { + const inputs = { + context: 'Test context', + model: 'gpt-4o', + apiKey: 'test-api-key', + routes: '{invalid json routes', + } + + await expect(handler.execute(mockContext, mockRouterV2Block, inputs)).rejects.toThrow( + 'No routes defined for router' + ) + }) + it('should handle fallback when JSON parsing fails', async () => { const inputs = { context: 'Test context', diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 1f3c7ba5745..25ce7ce1f7b 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -12,6 +12,7 @@ import { } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { buildAuthHeaders } from '@/executor/utils/http' +import { parseJSON, parseJSONOrThrow } from '@/executor/utils/json' import { resolveVertexCredential } from '@/executor/utils/vertex-credential' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -118,7 +119,9 @@ export class RouterBlockHandler implements BlockHandler { if (errorData.error) { errorMessage = errorData.error } - } catch (_e) {} + } catch (error) { + logger.warn('Failed to parse error response body', { error }) + } throw new Error(errorMessage) } @@ -267,7 +270,9 @@ export class RouterBlockHandler implements BlockHandler { if (errorData.error) { errorMessage = errorData.error } - } catch (_e) {} + } catch (error) { + logger.warn('Failed to parse error response body', { error }) + } throw new Error(errorMessage) } @@ -277,7 +282,7 @@ export class RouterBlockHandler implements BlockHandler { let reasoning = '' try { - const parsedResponse = JSON.parse(result.content) + const parsedResponse = parseJSONOrThrow(result.content) chosenRouteId = parsedResponse.route?.trim() || '' reasoning = parsedResponse.reasoning || '' } catch (_parseError) { @@ -367,18 +372,15 @@ export class RouterBlockHandler implements BlockHandler { * Parse routes from input (can be JSON string or array) */ private parseRoutes(input: any): RouteDefinition[] { - try { - if (typeof input === 'string') { - return JSON.parse(input) - } - if (Array.isArray(input)) { - return input - } - return [] - } catch (error) { - logger.error('Failed to parse routes:', { input, error }) + if (Array.isArray(input)) { + return input + } + const parsed = parseJSON(input, []) + if (!Array.isArray(parsed)) { + logger.error('Failed to parse routes:', { input }) return [] } + return parsed } private getTargetBlocks(ctx: ExecutionContext, block: SerializedBlock) { From 7308b531dcaf81056a29f9a692495bf5ecb1834d Mon Sep 17 00:00:00 2001 From: Ryan Smith Date: Fri, 29 May 2026 01:23:44 -0600 Subject: [PATCH 2/2] fix(executor): restore logging for invalid JSON in parseRoutes parseJSON(input, []) returns [] on parse failure, making the subsequent !Array.isArray check unreachable for the invalid-JSON case. Use null as the fallback to distinguish parse failure from valid-but- not-array, and log both cases separately. --- apps/sim/executor/handlers/router/router-handler.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 25ce7ce1f7b..2ba4341c92c 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -375,11 +375,15 @@ export class RouterBlockHandler implements BlockHandler { if (Array.isArray(input)) { return input } - const parsed = parseJSON(input, []) - if (!Array.isArray(parsed)) { + const parsed = parseJSON(input, null) + if (parsed === null) { logger.error('Failed to parse routes:', { input }) return [] } + if (!Array.isArray(parsed)) { + logger.error('Routes parsed but is not an array:', { input, parsed }) + return [] + } return parsed }