Skip to content

Commit d3fd0be

Browse files
Vikhyath MondretiVikhyath Mondreti
authored andcommitted
fix response format non-json input crash bug
1 parent ede224a commit d3fd0be

File tree

6 files changed

+239
-42
lines changed

6 files changed

+239
-42
lines changed

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,37 @@ 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)
200+
const responseFormatValue = blockState.responseFormat.trim()
204201

202+
// Check for variable references like <start.input>
203+
if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) {
204+
logger.debug(`[${requestId}] Response format contains variable reference for block ${blockId}`)
205+
// Keep variable references as-is - they will be resolved during execution
206+
acc[blockId] = blockState
207+
} else if (responseFormatValue === '') {
208+
// Empty string - remove response format
205209
acc[blockId] = {
206210
...blockState,
207-
responseFormat: parsedResponseFormat,
211+
responseFormat: undefined,
212+
}
213+
} else {
214+
try {
215+
logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`)
216+
// Attempt to parse the responseFormat if it's a string
217+
const parsedResponseFormat = JSON.parse(responseFormatValue)
218+
219+
acc[blockId] = {
220+
...blockState,
221+
responseFormat: parsedResponseFormat,
222+
}
223+
} catch (error) {
224+
logger.warn(`[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`, error)
225+
// Set to undefined instead of keeping malformed JSON - this allows execution to continue
226+
acc[blockId] = {
227+
...blockState,
228+
responseFormat: undefined,
229+
}
208230
}
209-
} catch (error) {
210-
logger.warn(`[${requestId}] Failed to parse responseFormat for block ${blockId}`, error)
211-
acc[blockId] = blockState
212231
}
213232
} else {
214233
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: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,36 @@ export interface ConnectedBlock {
2929
}
3030
}
3131

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

78108
let responseFormat
79109

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-
}
110+
// Safely parse response format with proper error handling
111+
responseFormat = parseResponseFormatSafely(responseFormatValue, sourceId)
89112

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

121144
let responseFormat
122145

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-
}
146+
// Safely parse response format with proper error handling
147+
responseFormat = parseResponseFormatSafely(responseFormatValue, edge.source)
132148

133149
// Get the default output type from the block's outputs
134150
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)