Skip to content

Commit 5167deb

Browse files
icecrasher321Vikhyath Mondreti
andauthored
fix(resp format): non-json input was crashing (#631)
* fix response format non-json input crash bug * fix lint --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
1 parent 02b7899 commit 5167deb

File tree

6 files changed

+244
-43
lines changed

6 files changed

+244
-43
lines changed

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,42 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
197197
(acc, [blockId, blockState]) => {
198198
// Check if this block has a responseFormat that needs to be parsed
199199
if (blockState.responseFormat && typeof blockState.responseFormat === 'string') {
200-
try {
201-
logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`)
202-
// Attempt to parse the responseFormat if it's a string
203-
const parsedResponseFormat = JSON.parse(blockState.responseFormat)
204-
200+
const responseFormatValue = blockState.responseFormat.trim()
201+
202+
// Check for variable references like <start.input>
203+
if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) {
204+
logger.debug(
205+
`[${requestId}] Response format contains variable reference for block ${blockId}`
206+
)
207+
// Keep variable references as-is - they will be resolved during execution
208+
acc[blockId] = blockState
209+
} else if (responseFormatValue === '') {
210+
// Empty string - remove response format
205211
acc[blockId] = {
206212
...blockState,
207-
responseFormat: parsedResponseFormat,
213+
responseFormat: undefined,
214+
}
215+
} else {
216+
try {
217+
logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`)
218+
// Attempt to parse the responseFormat if it's a string
219+
const parsedResponseFormat = JSON.parse(responseFormatValue)
220+
221+
acc[blockId] = {
222+
...blockState,
223+
responseFormat: parsedResponseFormat,
224+
}
225+
} catch (error) {
226+
logger.warn(
227+
`[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`,
228+
error
229+
)
230+
// Set to undefined instead of keeping malformed JSON - this allows execution to continue
231+
acc[blockId] = {
232+
...blockState,
233+
responseFormat: undefined,
234+
}
208235
}
209-
} catch (error) {
210-
logger.warn(`[${requestId}] Failed to parse responseFormat for block ${blockId}`, error)
211-
acc[blockId] = blockState
212236
}
213237
} else {
214238
acc[blockId] = blockState

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/response-format.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,13 @@ export function ResponseFormat({
290290
{showPreview && (
291291
<div className='rounded border bg-muted/30 p-2'>
292292
<pre className='max-h-32 overflow-auto text-xs'>
293-
{JSON.stringify(generateJSON(properties), null, 2)}
293+
{(() => {
294+
try {
295+
return JSON.stringify(generateJSON(properties), null, 2)
296+
} catch (error) {
297+
return `Error generating preview: ${error instanceof Error ? error.message : 'Unknown error'}`
298+
}
299+
})()}
294300
</pre>
295301
</div>
296302
)}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ export interface ConnectedBlock {
2929
}
3030
}
3131

32+
function parseResponseFormatSafely(responseFormatValue: any, blockId: string): any {
33+
if (!responseFormatValue) {
34+
return undefined
35+
}
36+
37+
if (typeof responseFormatValue === 'object' && responseFormatValue !== null) {
38+
return responseFormatValue
39+
}
40+
41+
if (typeof responseFormatValue === 'string') {
42+
const trimmedValue = responseFormatValue.trim()
43+
44+
if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
45+
return trimmedValue
46+
}
47+
48+
if (trimmedValue === '') {
49+
return undefined
50+
}
51+
52+
try {
53+
return JSON.parse(trimmedValue)
54+
} catch (error) {
55+
return undefined
56+
}
57+
}
58+
return undefined
59+
}
60+
3261
// Helper function to extract fields from JSON Schema
3362
function extractFieldsFromSchema(schema: any): Field[] {
3463
if (!schema || typeof schema !== 'object') {
@@ -77,15 +106,8 @@ export function useBlockConnections(blockId: string) {
77106

78107
let responseFormat
79108

80-
try {
81-
responseFormat =
82-
typeof responseFormatValue === 'string' && responseFormatValue
83-
? JSON.parse(responseFormatValue)
84-
: responseFormatValue // Handle case where it's already an object
85-
} catch (e) {
86-
logger.error('Failed to parse response format:', { e })
87-
responseFormat = undefined
88-
}
109+
// Safely parse response format with proper error handling
110+
responseFormat = parseResponseFormatSafely(responseFormatValue, sourceId)
89111

90112
// Get the default output type from the block's outputs
91113
const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({
@@ -120,15 +142,8 @@ export function useBlockConnections(blockId: string) {
120142

121143
let responseFormat
122144

123-
try {
124-
responseFormat =
125-
typeof responseFormatValue === 'string' && responseFormatValue
126-
? JSON.parse(responseFormatValue)
127-
: responseFormatValue // Handle case where it's already an object
128-
} catch (e) {
129-
logger.error('Failed to parse response format:', { e })
130-
responseFormat = undefined
131-
}
145+
// Safely parse response format with proper error handling
146+
responseFormat = parseResponseFormatSafely(responseFormatValue, edge.source)
132147

133148
// Get the default output type from the block's outputs
134149
const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -736,17 +736,90 @@ describe('AgentBlockHandler', () => {
736736
})
737737
})
738738

739-
it('should throw an error for invalid JSON in responseFormat', async () => {
739+
it('should handle invalid JSON in responseFormat gracefully', async () => {
740+
mockFetch.mockImplementationOnce(() => {
741+
return Promise.resolve({
742+
ok: true,
743+
headers: {
744+
get: (name: string) => {
745+
if (name === 'Content-Type') return 'application/json'
746+
if (name === 'X-Execution-Data') return null
747+
return null
748+
},
749+
},
750+
json: () =>
751+
Promise.resolve({
752+
content: 'Regular text response',
753+
model: 'mock-model',
754+
tokens: { prompt: 10, completion: 20, total: 30 },
755+
timing: { total: 100 },
756+
toolCalls: [],
757+
cost: undefined,
758+
}),
759+
})
760+
})
761+
740762
const inputs = {
741763
model: 'gpt-4o',
742764
userPrompt: 'Format this output.',
743765
apiKey: 'test-api-key',
744766
responseFormat: '{invalid-json',
745767
}
746768

747-
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
748-
'Invalid response'
749-
)
769+
// Should not throw an error, but continue with default behavior
770+
const result = await handler.execute(mockBlock, inputs, mockContext)
771+
772+
expect(result).toEqual({
773+
content: 'Regular text response',
774+
model: 'mock-model',
775+
tokens: { prompt: 10, completion: 20, total: 30 },
776+
toolCalls: { list: [], count: 0 },
777+
providerTiming: { total: 100 },
778+
cost: undefined,
779+
})
780+
})
781+
782+
it('should handle variable references in responseFormat gracefully', async () => {
783+
mockFetch.mockImplementationOnce(() => {
784+
return Promise.resolve({
785+
ok: true,
786+
headers: {
787+
get: (name: string) => {
788+
if (name === 'Content-Type') return 'application/json'
789+
if (name === 'X-Execution-Data') return null
790+
return null
791+
},
792+
},
793+
json: () =>
794+
Promise.resolve({
795+
content: 'Regular text response',
796+
model: 'mock-model',
797+
tokens: { prompt: 10, completion: 20, total: 30 },
798+
timing: { total: 100 },
799+
toolCalls: [],
800+
cost: undefined,
801+
}),
802+
})
803+
})
804+
805+
const inputs = {
806+
model: 'gpt-4o',
807+
userPrompt: 'Format this output.',
808+
apiKey: 'test-api-key',
809+
responseFormat: '<start.input>',
810+
}
811+
812+
// Should not throw an error, but continue with default behavior
813+
const result = await handler.execute(mockBlock, inputs, mockContext)
814+
815+
expect(result).toEqual({
816+
content: 'Regular text response',
817+
model: 'mock-model',
818+
tokens: { prompt: 10, completion: 20, total: 30 },
819+
toolCalls: { list: [], count: 0 },
820+
providerTiming: { total: 100 },
821+
cost: undefined,
822+
})
750823
})
751824

752825
it('should handle errors from the provider request', async () => {

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,63 @@ export class AgentBlockHandler implements BlockHandler {
5858
private parseResponseFormat(responseFormat?: string | object): any {
5959
if (!responseFormat || responseFormat === '') return undefined
6060

61-
try {
62-
const parsed =
63-
typeof responseFormat === 'string' ? JSON.parse(responseFormat) : responseFormat
64-
65-
if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) {
61+
// If already an object, process it directly
62+
if (typeof responseFormat === 'object' && responseFormat !== null) {
63+
const formatObj = responseFormat as any
64+
if (!formatObj.schema && !formatObj.name) {
6665
return {
6766
name: 'response_schema',
68-
schema: parsed,
67+
schema: responseFormat,
6968
strict: true,
7069
}
7170
}
72-
return parsed
73-
} catch (error: any) {
74-
logger.error('Failed to parse response format:', { error })
75-
throw new Error(`Invalid response format: ${error.message}`)
71+
return responseFormat
72+
}
73+
74+
// Handle string values
75+
if (typeof responseFormat === 'string') {
76+
const trimmedValue = responseFormat.trim()
77+
78+
// Check for variable references like <start.input>
79+
if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
80+
logger.info('Response format contains variable reference:', {
81+
value: trimmedValue,
82+
})
83+
// Variable references should have been resolved by the resolver before reaching here
84+
// If we still have a variable reference, it means it couldn't be resolved
85+
// Return undefined to use default behavior (no structured response)
86+
return undefined
87+
}
88+
89+
// Try to parse as JSON
90+
try {
91+
const parsed = JSON.parse(trimmedValue)
92+
93+
if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) {
94+
return {
95+
name: 'response_schema',
96+
schema: parsed,
97+
strict: true,
98+
}
99+
}
100+
return parsed
101+
} catch (error: any) {
102+
logger.warn('Failed to parse response format as JSON, using default behavior:', {
103+
error: error.message,
104+
value: trimmedValue,
105+
})
106+
// Return undefined instead of throwing - this allows execution to continue
107+
// without structured response format
108+
return undefined
109+
}
76110
}
111+
112+
// For any other type, return undefined
113+
logger.warn('Unexpected response format type, using default behavior:', {
114+
type: typeof responseFormat,
115+
value: responseFormat,
116+
})
117+
return undefined
77118
}
78119

79120
private async formatTools(inputTools: ToolInput[], context: ExecutionContext): Promise<any[]> {

apps/sim/serializer/index.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class Serializer {
121121
// Include response format fields if available
122122
...(params.responseFormat
123123
? {
124-
responseFormat: JSON.parse(params.responseFormat),
124+
responseFormat: this.parseResponseFormatSafely(params.responseFormat),
125125
}
126126
: {}),
127127
},
@@ -136,6 +136,48 @@ export class Serializer {
136136
}
137137
}
138138

139+
private parseResponseFormatSafely(responseFormat: any): any {
140+
if (!responseFormat) {
141+
return undefined
142+
}
143+
144+
// If already an object, return as-is
145+
if (typeof responseFormat === 'object' && responseFormat !== null) {
146+
return responseFormat
147+
}
148+
149+
// Handle string values
150+
if (typeof responseFormat === 'string') {
151+
const trimmedValue = responseFormat.trim()
152+
153+
// Check for variable references like <start.input>
154+
if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
155+
// Keep variable references as-is
156+
return trimmedValue
157+
}
158+
159+
if (trimmedValue === '') {
160+
return undefined
161+
}
162+
163+
// Try to parse as JSON
164+
try {
165+
return JSON.parse(trimmedValue)
166+
} catch (error) {
167+
// If parsing fails, return undefined to avoid crashes
168+
// This allows the workflow to continue without structured response format
169+
logger.warn('Failed to parse response format as JSON in serializer, using undefined:', {
170+
value: trimmedValue,
171+
error: error instanceof Error ? error.message : String(error),
172+
})
173+
return undefined
174+
}
175+
}
176+
177+
// For any other type, return undefined
178+
return undefined
179+
}
180+
139181
private extractParams(block: BlockState): Record<string, any> {
140182
// Special handling for subflow blocks (loops, parallels, etc.)
141183
if (block.type === 'loop' || block.type === 'parallel') {

0 commit comments

Comments
 (0)