From 177c6a50ccc71dcde596e53c515b502ebb853581 Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Sun, 5 Apr 2026 09:04:20 +1000 Subject: [PATCH] fix(executor): guard against missing content in provider responses When an LLM provider returns a 200 response without a content field, the router and evaluator handlers crash with a TypeError trying to call .trim() on undefined. This happens when a model is unavailable, rate-limited mid-stream, or returns an unexpected response structure. Add an explicit check for result.content before accessing it, matching the existing pattern in shared/response-format.ts (line 91) which already uses `result.content ?? ''` for this case. Affected handlers: - RouterBlockHandler (legacy path, line 127) - RouterBlockHandler V2 (line 287 fallback after JSON parse failure) - EvaluatorBlockHandler (line 148) Added regression tests for all three paths. --- .../evaluator/evaluator-handler.test.ts | 23 +++++++ .../handlers/evaluator/evaluator-handler.ts | 6 ++ .../handlers/router/router-handler.test.ts | 65 +++++++++++++++++++ .../handlers/router/router-handler.ts | 12 ++++ 4 files changed, 106 insertions(+) diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts index e70015a68bc..0175ee41fa7 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts @@ -287,6 +287,29 @@ describe('EvaluatorBlockHandler', () => { expect((result as any).fluency).toBe(0) }) + it('should throw a clear error when provider returns no content', async () => { + const inputs = { + content: 'Test content to evaluate', + metrics: [{ name: 'quality', description: 'Quality', range: { min: 0, max: 10 } }], + apiKey: 'test-api-key', + } + + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + model: 'gpt-4o', + tokens: { input: 50, output: 0, total: 50 }, + }), + }) + }) + + await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow( + 'Provider returned an empty response' + ) + }) + it('should extract metric scores ignoring case', async () => { const inputs = { content: 'Test', diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 95d25ba4656..f1370a983fa 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -145,6 +145,12 @@ export class EvaluatorBlockHandler implements BlockHandler { const result = await response.json() + if (!result.content) { + throw new Error( + 'Provider returned an empty response. The model may be unavailable or the request was malformed.' + ) + } + const parsedContent = this.extractJSONFromResponse(result.content) const metricScores = this.extractMetricScores(parsedContent, inputs.metrics) diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index 5d4defa0a0b..00ec7ff495e 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -244,6 +244,45 @@ describe('RouterBlockHandler', () => { await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow('Server error') }) + it('should throw a clear error when provider returns no content', async () => { + const inputs = { prompt: 'Choose the best option.', apiKey: 'test-api-key' } + + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + model: 'gpt-4o', + tokens: { input: 100, output: 0, total: 100 }, + }), + }) + }) + + await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow( + 'Provider returned an empty response' + ) + }) + + it('should throw a clear error when provider content is empty string', async () => { + const inputs = { prompt: 'Choose the best option.', apiKey: 'test-api-key' } + + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + content: '', + model: 'gpt-4o', + tokens: { input: 100, output: 0, total: 100 }, + }), + }) + }) + + await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow( + 'Provider returned an empty response' + ) + }) + it('should handle Azure OpenAI models with endpoint and API version', async () => { const inputs = { prompt: 'Choose the best option.', @@ -587,4 +626,30 @@ describe('RouterBlockHandler V2', () => { expect(result.selectedRoute).toBe('route-1') expect(result.reasoning).toBe('') }) + + it('should throw a clear error when provider returns no content (V2)', async () => { + const inputs = { + context: 'I need help with billing', + model: 'gpt-4o', + apiKey: 'test-api-key', + routes: JSON.stringify([ + { id: 'route-support', title: 'Support', value: 'Customer support inquiries' }, + ]), + } + + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + model: 'gpt-4o', + tokens: { input: 150, output: 0, total: 150 }, + }), + }) + }) + + await expect(handler.execute(mockContext, mockRouterV2Block, inputs)).rejects.toThrow( + 'Provider returned an empty response' + ) + }) }) diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index f4c715d2ced..fad573aa637 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -124,6 +124,12 @@ export class RouterBlockHandler implements BlockHandler { const result = await response.json() + if (!result.content) { + throw new Error( + 'Provider returned an empty response. The model may be unavailable or the request was malformed.' + ) + } + const chosenBlockId = result.content.trim().toLowerCase() const chosenBlock = targetBlocks?.find((b) => b.id === chosenBlockId) @@ -273,6 +279,12 @@ export class RouterBlockHandler implements BlockHandler { const result = await response.json() + if (!result.content) { + throw new Error( + 'Provider returned an empty response. The model may be unavailable or the request was malformed.' + ) + } + let chosenRouteId: string let reasoning = ''