mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
improvement(api): add native support for form-urlencoded inputs into API block (#1033)
This commit is contained in:
committed by
Siddharth Ganesan
parent
69773c3174
commit
d58ceb4bce
@@ -192,7 +192,18 @@ export class ToolTester<P = any, R = any> {
|
||||
const response = await this.mockFetch(url, {
|
||||
method: method,
|
||||
headers: this.tool.request.headers(params),
|
||||
body: this.tool.request.body ? JSON.stringify(this.tool.request.body(params)) : undefined,
|
||||
body: this.tool.request.body
|
||||
? (() => {
|
||||
const bodyResult = this.tool.request.body(params)
|
||||
const headers = this.tool.request.headers(params)
|
||||
const isPreformattedContent =
|
||||
headers['Content-Type'] === 'application/x-ndjson' ||
|
||||
headers['Content-Type'] === 'application/x-www-form-urlencoded'
|
||||
return isPreformattedContent && typeof bodyResult === 'string'
|
||||
? bodyResult
|
||||
: JSON.stringify(bodyResult)
|
||||
})()
|
||||
: undefined,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -109,6 +109,26 @@ describe('HTTP Request Tool', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.concurrent('should respect custom Content-Type headers', () => {
|
||||
// Custom Content-Type should not be overridden
|
||||
const headers = tester.getRequestHeaders({
|
||||
url: 'https://api.example.com',
|
||||
method: 'POST',
|
||||
body: { key: 'value' },
|
||||
headers: [{ Key: 'Content-Type', Value: 'application/x-www-form-urlencoded' }],
|
||||
})
|
||||
expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded')
|
||||
|
||||
// Case-insensitive Content-Type should not be overridden
|
||||
const headers2 = tester.getRequestHeaders({
|
||||
url: 'https://api.example.com',
|
||||
method: 'POST',
|
||||
body: { key: 'value' },
|
||||
headers: [{ Key: 'content-type', Value: 'text/plain' }],
|
||||
})
|
||||
expect(headers2['content-type']).toBe('text/plain')
|
||||
})
|
||||
|
||||
it('should set dynamic Referer header correctly', async () => {
|
||||
const originalWindow = global.window
|
||||
Object.defineProperty(global, 'window', {
|
||||
@@ -164,6 +184,30 @@ describe('HTTP Request Tool', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Body Construction', () => {
|
||||
it.concurrent('should handle JSON bodies correctly', () => {
|
||||
const body = { username: 'test', password: 'secret' }
|
||||
|
||||
expect(
|
||||
tester.getRequestBody({
|
||||
url: 'https://api.example.com',
|
||||
body,
|
||||
})
|
||||
).toEqual(body)
|
||||
})
|
||||
|
||||
it.concurrent('should handle FormData correctly', () => {
|
||||
const formData = { file: 'test.txt', content: 'file content' }
|
||||
|
||||
const result = tester.getRequestBody({
|
||||
url: 'https://api.example.com',
|
||||
formData,
|
||||
})
|
||||
|
||||
expect(result).toBeInstanceOf(FormData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Request Execution', () => {
|
||||
it('should apply default and dynamic headers to requests', async () => {
|
||||
// Setup mock response
|
||||
@@ -253,6 +297,59 @@ describe('HTTP Request Tool', () => {
|
||||
expect(bodyArg).toEqual(body)
|
||||
})
|
||||
|
||||
it('should handle POST requests with URL-encoded form data', async () => {
|
||||
// Setup mock response
|
||||
tester.setup({ result: 'success' })
|
||||
|
||||
// Create test body
|
||||
const body = { username: 'testuser123', password: 'testpass456', email: 'test@example.com' }
|
||||
|
||||
// Execute the tool with form-urlencoded content type
|
||||
await tester.execute({
|
||||
url: 'https://api.example.com/oauth/token',
|
||||
method: 'POST',
|
||||
body,
|
||||
headers: [{ cells: { Key: 'Content-Type', Value: 'application/x-www-form-urlencoded' } }],
|
||||
})
|
||||
|
||||
// Verify the request was made with correct headers
|
||||
const fetchCall = (global.fetch as any).mock.calls[0]
|
||||
expect(fetchCall[0]).toBe('https://api.example.com/oauth/token')
|
||||
expect(fetchCall[1].method).toBe('POST')
|
||||
expect(fetchCall[1].headers['Content-Type']).toBe('application/x-www-form-urlencoded')
|
||||
|
||||
// Verify the body is URL-encoded (should not be JSON stringified)
|
||||
expect(fetchCall[1].body).toBe(
|
||||
'username=testuser123&password=testpass456&email=test%40example.com'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle OAuth client credentials requests', async () => {
|
||||
// Setup mock response for OAuth token endpoint
|
||||
tester.setup({ access_token: 'token123', token_type: 'Bearer' })
|
||||
|
||||
// Execute OAuth client credentials request
|
||||
await tester.execute({
|
||||
url: 'https://oauth.example.com/token',
|
||||
method: 'POST',
|
||||
body: { grant_type: 'client_credentials', scope: 'read write' },
|
||||
headers: [
|
||||
{ cells: { Key: 'Content-Type', Value: 'application/x-www-form-urlencoded' } },
|
||||
{ cells: { Key: 'Authorization', Value: 'Basic Y2xpZW50OnNlY3JldA==' } },
|
||||
],
|
||||
})
|
||||
|
||||
// Verify the OAuth request was properly formatted
|
||||
const fetchCall = (global.fetch as any).mock.calls[0]
|
||||
expect(fetchCall[0]).toBe('https://oauth.example.com/token')
|
||||
expect(fetchCall[1].method).toBe('POST')
|
||||
expect(fetchCall[1].headers['Content-Type']).toBe('application/x-www-form-urlencoded')
|
||||
expect(fetchCall[1].headers.Authorization).toBe('Basic Y2xpZW50OnNlY3JldA==')
|
||||
|
||||
// Verify the body is URL-encoded
|
||||
expect(fetchCall[1].body).toBe('grant_type=client_credentials&scope=read+write')
|
||||
})
|
||||
|
||||
it('should handle errors correctly', async () => {
|
||||
// Setup error response
|
||||
tester.setup(mockHttpResponses.error, { ok: false, status: 400 })
|
||||
|
||||
@@ -67,12 +67,12 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
const processedUrl = processUrl(params.url, params.pathParams, params.params)
|
||||
const allHeaders = getDefaultHeaders(headers, processedUrl)
|
||||
|
||||
// Set appropriate Content-Type
|
||||
// Set appropriate Content-Type only if not already specified by user
|
||||
if (params.formData) {
|
||||
// Don't set Content-Type for FormData, browser will set it with boundary
|
||||
return allHeaders
|
||||
}
|
||||
if (params.body) {
|
||||
if (params.body && !allHeaders['Content-Type'] && !allHeaders['content-type']) {
|
||||
allHeaders['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
@@ -89,6 +89,24 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
}
|
||||
|
||||
if (params.body) {
|
||||
// Check if user wants URL-encoded form data
|
||||
const headers = transformTable(params.headers || null)
|
||||
const contentType = headers['Content-Type'] || headers['content-type']
|
||||
|
||||
if (
|
||||
contentType === 'application/x-www-form-urlencoded' &&
|
||||
typeof params.body === 'object'
|
||||
) {
|
||||
// Convert JSON object to URL-encoded string
|
||||
const urlencoded = new URLSearchParams()
|
||||
Object.entries(params.body).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
urlencoded.append(key, String(value))
|
||||
}
|
||||
})
|
||||
return urlencoded.toString()
|
||||
}
|
||||
|
||||
return params.body
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('formatRequestParams', () => {
|
||||
})
|
||||
|
||||
// Return a preformatted body
|
||||
mockTool.request.body = vi.fn().mockReturnValue({ body: 'key1=value1&key2=value2' })
|
||||
mockTool.request.body = vi.fn().mockReturnValue('key1=value1&key2=value2')
|
||||
|
||||
const params = { method: 'POST' }
|
||||
const result = formatRequestParams(mockTool, params)
|
||||
@@ -179,9 +179,7 @@ describe('formatRequestParams', () => {
|
||||
})
|
||||
|
||||
// Return a preformatted body for NDJSON
|
||||
mockTool.request.body = vi.fn().mockReturnValue({
|
||||
body: '{"prompt": "Hello"}\n{"prompt": "World"}',
|
||||
})
|
||||
mockTool.request.body = vi.fn().mockReturnValue('{"prompt": "Hello"}\n{"prompt": "World"}')
|
||||
|
||||
const params = { method: 'POST' }
|
||||
const result = formatRequestParams(mockTool, params)
|
||||
|
||||
@@ -63,8 +63,8 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
|
||||
headers['Content-Type'] === 'application/x-ndjson' ||
|
||||
headers['Content-Type'] === 'application/x-www-form-urlencoded'
|
||||
const body = hasBody
|
||||
? isPreformattedContent && bodyResult
|
||||
? bodyResult.body
|
||||
? isPreformattedContent && typeof bodyResult === 'string'
|
||||
? bodyResult
|
||||
: JSON.stringify(bodyResult)
|
||||
: undefined
|
||||
|
||||
|
||||
Reference in New Issue
Block a user