Skip to content

Commit b680ad4

Browse files
authored
fix(http): keep unwrapping legacy success envelopes for existing workflows (#9909)
#9889 fixed JSend-style 200s ({status, data}) being reported as failures by deleting the old /api/proxy envelope branch outright. That also changed two behaviors existing workflows rely on: 200 bodies with success/status/data were unwrapped (<api.data.x> read the inner data) and success: false failed the block. Restore both through one shared toHttpToolResponse helper, gated on the body carrying a success field — the only shape the old route ever produced — so only the misclassified case changes. Non-string envelope errors are now serialized instead of surfacing as [object Object].
1 parent c6d8c3c commit b680ad4

5 files changed

Lines changed: 98 additions & 41 deletions

File tree

‎apps/sim/tools/http/request.ts‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits'
22
import type { RequestParams, RequestResponse } from '@/tools/http/types'
3-
import { getCredentialHeaderNames, getDefaultHeaders, processUrl } from '@/tools/http/utils'
3+
import {
4+
getCredentialHeaderNames,
5+
getDefaultHeaders,
6+
processUrl,
7+
toHttpToolResponse,
8+
} from '@/tools/http/utils'
49
import { transformTable } from '@/tools/shared/table'
510
import type { ToolConfig } from '@/tools/types'
611

@@ -181,27 +186,14 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
181186
transformResponse: async (response: Response) => {
182187
const contentType = response.headers.get('content-type') || ''
183188

184-
const headers: Record<string, string> = {}
185-
response.headers.forEach((value, key) => {
186-
headers[key] = value
187-
})
188-
189189
const responseText = await readResponseTextWithLimit(response, {
190190
maxBytes: MAX_HTTP_RESPONSE_BODY_BYTES,
191191
label: 'HTTP Request response body',
192192
allowNoBodyFallback: true,
193193
})
194194
const data = contentType.includes('application/json') ? JSON.parse(responseText) : responseText
195195

196-
return {
197-
success: response.ok,
198-
output: {
199-
data,
200-
status: response.status,
201-
headers,
202-
},
203-
error: undefined, // Errors are handled upstream in executeTool
204-
}
196+
return toHttpToolResponse(response, data)
205197
},
206198

207199
outputs: {

‎apps/sim/tools/http/types.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export interface RequestParams {
2121
export interface RequestResponse extends ToolResponse {
2222
output: {
2323
data: unknown
24-
status: number
25-
headers: Record<string, string>
24+
/** The HTTP status code, or the body's own `status` for a legacy envelope. */
25+
status: unknown
26+
headers: Record<string, unknown>
2627
}
2728
}
2829

‎apps/sim/tools/http/utils.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { isRecordLike, toRecord } from '@sim/utils/object'
12
import { escapeRegExp } from '@sim/utils/string'
3+
import type { RequestResponse } from '@/tools/http/types'
24
import { transformTable } from '@/tools/shared/table'
35
import type { TableRow } from '@/tools/types'
46

@@ -153,3 +155,44 @@ export const processUrl = (
153155

154156
return canonicalizeFirstPartyApiUrl(url)
155157
}
158+
159+
/**
160+
* Builds the `http_request` / `webhook_request` result from a 2xx response and its parsed
161+
* body; non-2xx responses are rejected upstream in `executeTool`.
162+
*
163+
* A JSON body shaped like the envelope the removed `/api/proxy` route returned
164+
* (`{ success, status, data, headers? }`) is unwrapped exactly as these tools always have,
165+
* so existing workflows keep reading the same outputs and taking the same error branch.
166+
* A body with no `success` field (JSend's `{ status, data }`, for example) was never that
167+
* envelope and is reported as the HTTP response it is.
168+
*/
169+
export function toHttpToolResponse(response: Response, body: unknown): RequestResponse {
170+
if (
171+
isRecordLike(body) &&
172+
body.success !== undefined &&
173+
body.data !== undefined &&
174+
body.status !== undefined
175+
) {
176+
const success = Boolean(body.success)
177+
const { error } = body
178+
return {
179+
success,
180+
output: { data: body.data, status: body.status, headers: toRecord(body.headers) },
181+
error:
182+
success || error == null
183+
? undefined
184+
: typeof error === 'string'
185+
? error
186+
: JSON.stringify(error),
187+
}
188+
}
189+
190+
return {
191+
success: response.ok,
192+
output: {
193+
data: body,
194+
status: response.status,
195+
headers: Object.fromEntries(response.headers.entries()),
196+
},
197+
}
198+
}

‎apps/sim/tools/http/webhook_request.ts‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { hmacSha256Hex } from '@sim/security/hmac'
22
import { generateId } from '@sim/utils/id'
33
import type { RequestResponse, WebhookRequestParams } from '@/tools/http/types'
4+
import { toHttpToolResponse } from '@/tools/http/utils'
45
import type { ToolConfig } from '@/tools/types'
56

67
/**
@@ -76,24 +77,11 @@ export const webhookRequestTool: ToolConfig<WebhookRequestParams, RequestRespons
7677
transformResponse: async (response: Response) => {
7778
const contentType = response.headers.get('content-type') || ''
7879

79-
const headers: Record<string, string> = {}
80-
response.headers.forEach((value, key) => {
81-
headers[key] = value
82-
})
83-
8480
const data = await (contentType.includes('application/json')
8581
? response.json()
8682
: response.text())
8783

88-
return {
89-
success: response.ok,
90-
output: {
91-
data,
92-
status: response.status,
93-
headers,
94-
},
95-
error: undefined,
96-
}
84+
return toHttpToolResponse(response, data)
9785
},
9886

9987
outputs: {

‎apps/sim/tools/index.test.ts‎

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5543,20 +5543,21 @@ describe('HTTP tools report the real HTTP outcome', () => {
55435543
Reflect.deleteProperty(tools, 'webhook_request')
55445544
})
55455545

5546-
it.each([
5546+
const httpTools = [
55475547
['http_request', { url: 'https://api.example.com/orders', method: 'POST', body: {} }],
55485548
['webhook_request', { url: 'https://api.example.com/hooks', body: {} }],
5549-
])(
5549+
] as const
5550+
5551+
function respondWith(body: unknown) {
5552+
mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
5553+
toSecureFetchResponse(Response.json(body, { headers: { 'x-request-id': 'req-123' } }))
5554+
)
5555+
}
5556+
5557+
it.each(httpTools)(
55505558
'%s treats a 200 whose body has top-level status and data as a success',
55515559
async (toolId, params) => {
5552-
mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
5553-
toSecureFetchResponse(
5554-
Response.json(
5555-
{ status: 'success', data: { orderId: 'A-1001' } },
5556-
{ headers: { 'x-request-id': 'req-123' } }
5557-
)
5558-
)
5559-
)
5560+
respondWith({ status: 'success', data: { orderId: 'A-1001' } })
55605561

55615562
const result = await executeTool(toolId, params, { skipPostProcess: true })
55625563

@@ -5570,6 +5571,38 @@ describe('HTTP tools report the real HTTP outcome', () => {
55705571
})
55715572
}
55725573
)
5574+
5575+
it.each(httpTools)(
5576+
'%s keeps unwrapping a 200 body with success, status, and data',
5577+
async (toolId, params) => {
5578+
respondWith({ success: true, status: 'ok', data: { orderId: 'A-1001' } })
5579+
5580+
const result = await executeTool(toolId, params, { skipPostProcess: true })
5581+
5582+
expect(result, result.error).toMatchObject({
5583+
success: true,
5584+
output: { status: 'ok', data: { orderId: 'A-1001' }, headers: {} },
5585+
})
5586+
}
5587+
)
5588+
5589+
it.each([
5590+
...httpTools.map(
5591+
([toolId, params]) => [toolId, params, 'Out of stock', 'Out of stock'] as const
5592+
),
5593+
...httpTools.map(
5594+
([toolId, params]) => [toolId, params, { code: 'OOS' }, '{"code":"OOS"}'] as const
5595+
),
5596+
])(
5597+
'%s keeps failing a 200 body that reports success: false',
5598+
async (toolId, params, error, expectedError) => {
5599+
respondWith({ success: false, status: 'error', data: null, error })
5600+
5601+
const result = await executeTool(toolId, params, { skipPostProcess: true })
5602+
5603+
expect(result).toMatchObject({ success: false, error: expectedError })
5604+
}
5605+
)
55735606
})
55745607

55755608
describe('File Parameter Normalization', () => {

0 commit comments

Comments
 (0)