Skip to content

Commit d4f412a

Browse files
authored
fix(api): fix api post and get without stringifying (#955)
1 parent 70fa628 commit d4f412a

File tree

5 files changed

+68
-116
lines changed

5 files changed

+68
-116
lines changed

apps/sim/app/api/proxy/route.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,14 @@ export async function POST(request: Request) {
167167

168168
try {
169169
// Parse request body
170-
const requestText = await request.text()
171170
let requestBody
172171
try {
173-
requestBody = JSON.parse(requestText)
172+
requestBody = await request.json()
174173
} catch (parseError) {
175-
logger.error(`[${requestId}] Failed to parse request body: ${requestText}`, {
174+
logger.error(`[${requestId}] Failed to parse request body`, {
176175
error: parseError instanceof Error ? parseError.message : String(parseError),
177176
})
178-
throw new Error(`Invalid JSON in request body: ${requestText}`)
177+
throw new Error('Invalid JSON in request body')
179178
}
180179

181180
const { toolId, params, executionContext } = requestBody

apps/sim/tools/__test-utils__/test-tools.ts

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,37 +40,22 @@ export function createMockFetch(
4040
) {
4141
const { ok = true, status = 200, headers = { 'Content-Type': 'application/json' } } = options
4242

43-
// Normalize header keys to lowercase for case-insensitive access
44-
const normalizedHeaders: Record<string, string> = {}
45-
Object.entries(headers).forEach(([key, value]) => (normalizedHeaders[key.toLowerCase()] = value))
46-
47-
const makeResponse = () => {
48-
const jsonMock = vi.fn().mockResolvedValue(responseData)
49-
const textMock = vi
43+
const mockFn = vi.fn().mockResolvedValue({
44+
ok,
45+
status,
46+
headers: {
47+
get: (key: string) => headers[key.toLowerCase()],
48+
forEach: (callback: (value: string, key: string) => void) => {
49+
Object.entries(headers).forEach(([key, value]) => callback(value, key))
50+
},
51+
},
52+
json: vi.fn().mockResolvedValue(responseData),
53+
text: vi
5054
.fn()
5155
.mockResolvedValue(
5256
typeof responseData === 'string' ? responseData : JSON.stringify(responseData)
53-
)
54-
55-
const res: any = {
56-
ok,
57-
status,
58-
headers: {
59-
get: (key: string) => normalizedHeaders[key.toLowerCase()],
60-
forEach: (callback: (value: string, key: string) => void) => {
61-
Object.entries(normalizedHeaders).forEach(([key, value]) => callback(value, key))
62-
},
63-
},
64-
json: jsonMock,
65-
text: textMock,
66-
}
67-
68-
// Implement clone() so production code that clones responses keeps working in tests
69-
res.clone = vi.fn().mockImplementation(() => makeResponse())
70-
return res
71-
}
72-
73-
const mockFn = vi.fn().mockResolvedValue(makeResponse())
57+
),
58+
})
7459

7560
// Add preconnect property to satisfy TypeScript
7661

apps/sim/tools/http/request.ts

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { RequestParams, RequestResponse } from '@/tools/http/types'
2-
import { getDefaultHeaders, processUrl, shouldUseProxy, transformTable } from '@/tools/http/utils'
2+
import { getDefaultHeaders, processUrl, transformTable } from '@/tools/http/utils'
33
import type { ToolConfig } from '@/tools/types'
44

55
export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
@@ -53,34 +53,18 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
5353

5454
request: {
5555
url: (params: RequestParams) => {
56-
// Process the URL first to handle path/query params
57-
const processedUrl = processUrl(params.url, params.pathParams, params.params)
58-
59-
// For external URLs that need proxying in the browser, we still return the
60-
// external URL here and let executeTool route through the POST /api/proxy
61-
// endpoint uniformly. This avoids querystring body encoding and prevents
62-
// the proxy GET route from being hit from the client.
63-
if (shouldUseProxy(processedUrl)) {
64-
return processedUrl
65-
}
66-
67-
return processedUrl
56+
// Process the URL once and cache the result
57+
return processUrl(params.url, params.pathParams, params.params)
6858
},
6959

70-
method: (params: RequestParams) => params.method || 'GET',
60+
method: (params: RequestParams) => {
61+
// Always return the user's intended method - executeTool handles proxy routing
62+
return params.method || 'GET'
63+
},
7164

7265
headers: (params: RequestParams) => {
7366
const headers = transformTable(params.headers || null)
7467
const processedUrl = processUrl(params.url, params.pathParams, params.params)
75-
76-
// For proxied requests, we only need minimal headers
77-
if (shouldUseProxy(processedUrl)) {
78-
return {
79-
'Content-Type': 'application/json',
80-
}
81-
}
82-
83-
// For direct requests, add all our standard headers
8468
const allHeaders = getDefaultHeaders(headers, processedUrl)
8569

8670
// Set appropriate Content-Type
@@ -96,13 +80,6 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
9680
},
9781

9882
body: (params: RequestParams) => {
99-
const processedUrl = processUrl(params.url, params.pathParams, params.params)
100-
101-
// For proxied requests, we don't need a body
102-
if (shouldUseProxy(processedUrl)) {
103-
return undefined
104-
}
105-
10683
if (params.formData) {
10784
const formData = new FormData()
10885
Object.entries(params.formData).forEach(([key, value]) => {
@@ -120,63 +97,46 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
12097
},
12198

12299
transformResponse: async (response: Response) => {
123-
// Build headers once for consistent return structures
100+
const contentType = response.headers.get('content-type') || ''
101+
102+
// Standard response handling
124103
const headers: Record<string, string> = {}
125104
response.headers.forEach((value, key) => {
126105
headers[key] = value
127106
})
128107

129-
const contentType = response.headers.get('content-type') || ''
130-
const isJson = contentType.includes('application/json')
131-
132-
if (isJson) {
133-
// Use a clone to safely inspect JSON without consuming the original body
134-
let jsonResponse: any
135-
try {
136-
jsonResponse = await response.clone().json()
137-
} catch (_e) {
138-
jsonResponse = undefined
139-
}
140-
141-
// Proxy responses wrap the real payload
142-
if (jsonResponse && jsonResponse.data !== undefined && jsonResponse.status !== undefined) {
143-
return {
144-
success: jsonResponse.success,
145-
output: {
146-
data: jsonResponse.data,
147-
status: jsonResponse.status,
148-
headers: jsonResponse.headers || {},
149-
},
150-
error: jsonResponse.success
151-
? undefined
152-
: jsonResponse.data && typeof jsonResponse.data === 'object' && jsonResponse.data.error
153-
? `HTTP error ${jsonResponse.status}: ${jsonResponse.data.error.message || JSON.stringify(jsonResponse.data.error)}`
154-
: jsonResponse.error || `HTTP error ${jsonResponse.status}`,
155-
}
156-
}
157-
158-
// Non-proxy JSON response: return parsed JSON directly
108+
const data = await (contentType.includes('application/json')
109+
? response.json()
110+
: response.text())
111+
112+
// Check if this is a proxy response (structured response from /api/proxy)
113+
if (
114+
contentType.includes('application/json') &&
115+
typeof data === 'object' &&
116+
data !== null &&
117+
data.data !== undefined &&
118+
data.status !== undefined
119+
) {
159120
return {
160-
success: response.ok,
121+
success: data.success,
161122
output: {
162-
data: jsonResponse ?? (await response.text()),
163-
status: response.status,
164-
headers,
123+
data: data.data,
124+
status: data.status,
125+
headers: data.headers || {},
165126
},
166-
error: response.ok ? undefined : `HTTP error ${response.status}: ${response.statusText}`,
127+
error: data.success ? undefined : data.error,
167128
}
168129
}
169130

170-
// Non-JSON response: return text
171-
const textData = await response.text()
131+
// Direct response handling
172132
return {
173133
success: response.ok,
174134
output: {
175-
data: textData,
135+
data,
176136
status: response.status,
177137
headers,
178138
},
179-
error: response.ok ? undefined : `HTTP error ${response.status}: ${response.statusText}`,
139+
error: undefined, // Errors are handled upstream in executeTool
180140
}
181141
},
182142

apps/sim/tools/index.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,7 @@ export async function executeTool(
200200
}
201201
}
202202

203-
// For external APIs, always use the proxy POST, and ensure the tool request
204-
// builds a direct external URL (not the querystring proxy variant)
203+
// For external APIs, use the proxy
205204
const result = await handleProxyRequest(toolId, contextParams, executionContext)
206205

207206
// Apply post-processing if available and not skipped
@@ -396,13 +395,10 @@ async function handleInternalRequest(
396395

397396
const response = await fetch(fullUrl, requestOptions)
398397

399-
// Clone the response for error checking while preserving original for transformResponse
400-
const responseForErrorCheck = response.clone()
401-
402-
// Parse response data for error checking
398+
// Parse response data once
403399
let responseData
404400
try {
405-
responseData = await responseForErrorCheck.json()
401+
responseData = await response.json()
406402
} catch (jsonError) {
407403
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
408404
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
@@ -411,7 +407,7 @@ async function handleInternalRequest(
411407
}
412408

413409
// Check for error conditions
414-
const { isError, errorInfo } = isErrorResponse(responseForErrorCheck, responseData)
410+
const { isError, errorInfo } = isErrorResponse(response, responseData)
415411

416412
if (isError) {
417413
// Handle error case
@@ -465,7 +461,18 @@ async function handleInternalRequest(
465461
// Success case: use transformResponse if available
466462
if (tool.transformResponse) {
467463
try {
468-
const data = await tool.transformResponse(response, params)
464+
// Create a mock response object that provides the methods transformResponse needs
465+
const mockResponse = {
466+
ok: response.ok,
467+
status: response.status,
468+
statusText: response.statusText,
469+
headers: response.headers,
470+
json: async () => responseData,
471+
text: async () =>
472+
typeof responseData === 'string' ? responseData : JSON.stringify(responseData),
473+
} as Response
474+
475+
const data = await tool.transformResponse(mockResponse, params)
469476
return data
470477
} catch (transformError) {
471478
logger.error(`[${requestId}] Transform response error for ${toolId}:`, {

apps/sim/tools/utils.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,18 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
4545
// Process URL
4646
const url = typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
4747

48-
// Process method (support function or string on tool.request.method)
49-
const methodFromTool =
50-
typeof tool.request.method === 'function' ? tool.request.method(params) : tool.request.method
51-
const method = (params.method || methodFromTool || 'GET').toUpperCase()
48+
// Process method
49+
const method =
50+
typeof tool.request.method === 'function'
51+
? tool.request.method(params)
52+
: params.method || tool.request.method || 'GET'
5253

5354
// Process headers
5455
const headers = tool.request.headers ? tool.request.headers(params) : {}
5556

5657
// Process body
58+
const hasBody = method !== 'GET' && method !== 'HEAD' && !!tool.request.body
5759
const bodyResult = tool.request.body ? tool.request.body(params) : undefined
58-
const hasBody = method !== 'GET' && method !== 'HEAD' && bodyResult !== undefined
5960

6061
// Special handling for NDJSON content type or 'application/x-www-form-urlencoded'
6162
const isPreformattedContent =

0 commit comments

Comments
 (0)