import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' import type { RequestParams, RequestResponse } from '@/tools/http/types' import { getDefaultHeaders, processUrl } from '@/tools/http/utils' import { transformTable } from '@/tools/shared/table' import type { ToolConfig } from '@/tools/types' const MAX_HTTP_RESPONSE_BODY_BYTES = 10 * 1024 * 1024 export const requestTool: ToolConfig = { id: 'http_request', name: 'HTTP Request', description: 'Make HTTP requests with comprehensive support for methods, headers, query parameters, path parameters, and form data. Features configurable timeout and status validation for robust API interactions.', version: '1.0.0', params: { url: { type: 'string', required: true, visibility: 'user-or-llm', description: 'The URL to send the request to', }, method: { type: 'string', default: 'GET', visibility: 'user-or-llm', description: 'HTTP method (GET, POST, PUT, PATCH, DELETE)', }, headers: { type: 'object', visibility: 'user-or-llm', description: 'HTTP headers to include', }, body: { type: 'object', visibility: 'user-or-llm', description: 'Request body (for POST, PUT, PATCH)', }, params: { type: 'object', visibility: 'user-or-llm', description: 'URL query parameters to append', }, pathParams: { type: 'object', visibility: 'user-or-llm', description: 'URL path parameters to replace (e.g., :id in /users/:id)', }, formData: { type: 'object', visibility: 'user-or-llm', description: 'Form data to send (will set appropriate Content-Type)', }, timeout: { type: 'number', visibility: 'user-only', description: 'Request timeout in milliseconds (default: 300000 = 5 minutes)', }, retries: { type: 'number', visibility: 'hidden', description: 'Number of retry attempts for retryable failures (timeouts, 429, 5xx). Default: 0 (no retries).', }, retryDelayMs: { type: 'number', visibility: 'hidden', description: 'Initial retry delay in milliseconds (default: 500)', }, retryMaxDelayMs: { type: 'number', visibility: 'hidden', description: 'Maximum delay between retries in milliseconds (default: 30000)', }, retryNonIdempotent: { type: 'boolean', visibility: 'hidden', description: 'Allow retries for non-idempotent methods like POST/PATCH (may create duplicate requests).', }, }, request: { url: (params: RequestParams) => { return processUrl(params.url, params.pathParams, params.params) }, method: (params: RequestParams) => { // Always return the user's intended method - executeTool handles proxy routing return params.method || 'GET' }, headers: (params: RequestParams) => { const headers = transformTable(params.headers || null) const processedUrl = processUrl(params.url, params.pathParams, params.params) const allHeaders = getDefaultHeaders(headers, processedUrl) // 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 && !allHeaders['Content-Type'] && !allHeaders['content-type']) { allHeaders['Content-Type'] = 'application/json' } return allHeaders }, body: ((params: RequestParams) => { if (params.formData) { const formData = new FormData() Object.entries(params.formData).forEach(([key, value]) => { formData.append(key, value) }) return formData } if (params.body) { 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 as Record).forEach(([key, value]) => { if (value !== undefined && value !== null) { urlencoded.append( key, typeof value === 'object' ? JSON.stringify(value) : String(value) ) } }) return urlencoded.toString() } return params.body as Record } return undefined }) as (params: RequestParams) => Record | string | FormData | undefined, retry: { enabled: true, maxRetries: 0, initialDelayMs: 500, maxDelayMs: 30000, retryIdempotentOnly: true, }, }, transformResponse: async (response: Response) => { const contentType = response.headers.get('content-type') || '' // Standard response handling const headers: Record = {} response.headers.forEach((value, key) => { headers[key] = value }) const responseText = await readResponseTextWithLimit(response, { maxBytes: MAX_HTTP_RESPONSE_BODY_BYTES, label: 'HTTP Request response body', allowNoBodyFallback: true, }) const data = contentType.includes('application/json') ? JSON.parse(responseText) : responseText // Check if this is a proxy response (structured response from /api/proxy) if ( contentType.includes('application/json') && typeof data === 'object' && data !== null && data.data !== undefined && data.status !== undefined ) { return { success: data.success, output: { data: data.data, status: data.status, headers: data.headers || {}, }, error: data.success ? undefined : data.error, } } // Direct response handling return { success: response.ok, output: { data, status: response.status, headers, }, error: undefined, // Errors are handled upstream in executeTool } }, outputs: { data: { type: 'json', description: 'Response data from the HTTP request (JSON object, text, or other format)', }, status: { type: 'number', description: 'HTTP status code of the response (e.g., 200, 404, 500)', }, headers: { type: 'object', description: 'Response headers as key-value pairs', properties: { 'content-type': { type: 'string', description: 'Content type of the response', optional: true, }, 'content-length': { type: 'string', description: 'Content length', optional: true }, }, }, }, }