Merge pull request #954 from simstudioai/staging

fix
This commit is contained in:
Waleed Latif
2025-08-12 21:12:18 -07:00
committed by GitHub
5 changed files with 80 additions and 68 deletions

View File

@@ -167,14 +167,15 @@ export async function POST(request: Request) {
try {
// Parse request body
const requestText = await request.text()
let requestBody
try {
requestBody = await request.json()
requestBody = JSON.parse(requestText)
} catch (parseError) {
logger.error(`[${requestId}] Failed to parse request body`, {
logger.error(`[${requestId}] Failed to parse request body: ${requestText}`, {
error: parseError instanceof Error ? parseError.message : String(parseError),
})
throw new Error('Invalid JSON in request body')
throw new Error(`Invalid JSON in request body: ${requestText}`)
}
const { toolId, params, executionContext } = requestBody

View File

@@ -40,22 +40,37 @@ export function createMockFetch(
) {
const { ok = true, status = 200, headers = { 'Content-Type': 'application/json' } } = options
const mockFn = vi.fn().mockResolvedValue({
ok,
status,
headers: {
get: (key: string) => headers[key.toLowerCase()],
forEach: (callback: (value: string, key: string) => void) => {
Object.entries(headers).forEach(([key, value]) => callback(value, key))
},
},
json: vi.fn().mockResolvedValue(responseData),
text: vi
// Normalize header keys to lowercase for case-insensitive access
const normalizedHeaders: Record<string, string> = {}
Object.entries(headers).forEach(([key, value]) => (normalizedHeaders[key.toLowerCase()] = value))
const makeResponse = () => {
const jsonMock = vi.fn().mockResolvedValue(responseData)
const textMock = vi
.fn()
.mockResolvedValue(
typeof responseData === 'string' ? responseData : JSON.stringify(responseData)
),
})
)
const res: any = {
ok,
status,
headers: {
get: (key: string) => normalizedHeaders[key.toLowerCase()],
forEach: (callback: (value: string, key: string) => void) => {
Object.entries(normalizedHeaders).forEach(([key, value]) => callback(value, key))
},
},
json: jsonMock,
text: textMock,
}
// Implement clone() so production code that clones responses keeps working in tests
res.clone = vi.fn().mockImplementation(() => makeResponse())
return res
}
const mockFn = vi.fn().mockResolvedValue(makeResponse())
// Add preconnect property to satisfy TypeScript

View File

@@ -56,29 +56,12 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
// Process the URL first to handle path/query params
const processedUrl = processUrl(params.url, params.pathParams, params.params)
// For external URLs that need proxying
// For external URLs that need proxying in the browser, we still return the
// external URL here and let executeTool route through the POST /api/proxy
// endpoint uniformly. This avoids querystring body encoding and prevents
// the proxy GET route from being hit from the client.
if (shouldUseProxy(processedUrl)) {
let proxyUrl = `/api/proxy?url=${encodeURIComponent(processedUrl)}`
if (params.method) {
proxyUrl += `&method=${encodeURIComponent(params.method)}`
}
if (params.body && ['POST', 'PUT', 'PATCH'].includes(params.method?.toUpperCase() || '')) {
const bodyStr =
typeof params.body === 'string' ? params.body : JSON.stringify(params.body)
proxyUrl += `&body=${encodeURIComponent(bodyStr)}`
}
// Forward all headers as URL parameters
const userHeaders = transformTable(params.headers || null)
for (const [key, value] of Object.entries(userHeaders)) {
if (value !== undefined && value !== null) {
proxyUrl += `&header.${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
}
}
return proxyUrl
return processedUrl
}
return processedUrl
@@ -137,13 +120,26 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
},
transformResponse: async (response: Response) => {
// For proxy responses, we need to parse the JSON and extract the data
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('application/json')) {
const jsonResponse = await response.json()
// Build headers once for consistent return structures
const headers: Record<string, string> = {}
response.headers.forEach((value, key) => {
headers[key] = value
})
// Check if this is a proxy response
if (jsonResponse.data !== undefined && jsonResponse.status !== undefined) {
const contentType = response.headers.get('content-type') || ''
const isJson = contentType.includes('application/json')
if (isJson) {
// Use a clone to safely inspect JSON without consuming the original body
let jsonResponse: any
try {
jsonResponse = await response.clone().json()
} catch (_e) {
jsonResponse = undefined
}
// Proxy responses wrap the real payload
if (jsonResponse && jsonResponse.data !== undefined && jsonResponse.status !== undefined) {
return {
success: jsonResponse.success,
output: {
@@ -153,32 +149,30 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
},
error: jsonResponse.success
? undefined
: // Extract and display the actual API error message from the response if available
jsonResponse.data && typeof jsonResponse.data === 'object' && jsonResponse.data.error
: jsonResponse.data && typeof jsonResponse.data === 'object' && jsonResponse.data.error
? `HTTP error ${jsonResponse.status}: ${jsonResponse.data.error.message || JSON.stringify(jsonResponse.data.error)}`
: jsonResponse.error || `HTTP error ${jsonResponse.status}`,
}
}
// Non-proxy JSON response: return parsed JSON directly
return {
success: response.ok,
output: {
data: jsonResponse ?? (await response.text()),
status: response.status,
headers,
},
error: response.ok ? undefined : `HTTP error ${response.status}: ${response.statusText}`,
}
}
// Standard response handling
const headers: Record<string, string> = {}
response.headers.forEach((value, key) => {
headers[key] = value
})
let data
try {
data = await (contentType.includes('application/json') ? response.json() : response.text())
} catch (error) {
// If response body reading fails, we can't retry reading - just use error message
data = `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`
}
// Non-JSON response: return text
const textData = await response.text()
return {
success: response.ok,
output: {
data,
data: textData,
status: response.status,
headers,
},

View File

@@ -204,7 +204,8 @@ export async function executeTool(
}
}
// For external APIs, use the proxy
// For external APIs, always use the proxy POST, and ensure the tool request
// builds a direct external URL (not the querystring proxy variant)
const result = await handleProxyRequest(toolId, contextParams, executionContext)
// Apply post-processing if available and not skipped
@@ -399,9 +400,8 @@ async function handleInternalRequest(
const response = await fetch(fullUrl, requestOptions)
// Clone the response immediately before any body consumption
// Clone the response for error checking while preserving original for transformResponse
const responseForErrorCheck = response.clone()
const responseForTransform = response.clone()
// Parse response data for error checking
let responseData
@@ -469,7 +469,7 @@ async function handleInternalRequest(
// Success case: use transformResponse if available
if (tool.transformResponse) {
try {
const data = await tool.transformResponse(responseForTransform, params)
const data = await tool.transformResponse(response, params)
return data
} catch (transformError) {
logger.error(`[${requestId}] Transform response error for ${toolId}:`, {

View File

@@ -45,15 +45,17 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
// Process URL
const url = typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
// Process method
const method = params.method || tool.request.method || 'GET'
// Process method (support function or string on tool.request.method)
const methodFromTool =
typeof tool.request.method === 'function' ? tool.request.method(params) : tool.request.method
const method = (params.method || methodFromTool || 'GET').toUpperCase()
// Process headers
const headers = tool.request.headers ? tool.request.headers(params) : {}
// Process body
const hasBody = method !== 'GET' && method !== 'HEAD' && !!tool.request.body
const bodyResult = tool.request.body ? tool.request.body(params) : undefined
const hasBody = method !== 'GET' && method !== 'HEAD' && bodyResult !== undefined
// Special handling for NDJSON content type or 'application/x-www-form-urlencoded'
const isPreformattedContent =