Skip to content

Commit 26aa582

Browse files
committed
fix(http): substitute path parameters only by their whole name
The HTTP request tool replaced each path parameter with a plain string replace of `:${key}` over the whole URL. An empty key stripped the colon from the scheme, so `https://host` became `https//host` and the request was refused as not absolute; a numeric key rewrote the port into the host; and `:id` matched inside `:idx`. Models occasionally send an empty path-parameter entry, which made agent API tool calls fail intermittently. Substitute a key only when it starts like a JavaScript identifier, as path-to-regexp defines `:name` parameters, and end each placeholder where an identifier ends.
1 parent 59a364a commit 26aa582

2 files changed

Lines changed: 55 additions & 3 deletions

File tree

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,34 @@ describe('HTTP Request Tool', () => {
7070
expect(url.includes('chars')).toBe(true)
7171
})
7272

73+
it.concurrent('substitutes path parameters only by their whole name', () => {
74+
expect(processUrl('https://www.google.com', { '': '' })).toBe('https://www.google.com')
75+
expect(processUrl('https://api.example.com:8443/users/:id', { '8443': 'x', id: '42' })).toBe(
76+
'https://api.example.com:8443/users/42'
77+
)
78+
expect(processUrl('https://api.example.com/users/:idx/:id', { id: '1', idx: '2' })).toBe(
79+
'https://api.example.com/users/2/1'
80+
)
81+
expect(processUrl('https://api.example.com/users/:id-profile', { id: '7' })).toBe(
82+
'https://api.example.com/users/7-profile'
83+
)
84+
expect(processUrl('https://api.example.com/users/:user-id', { 'user-id': '9' })).toBe(
85+
'https://api.example.com/users/9'
86+
)
87+
expect(processUrl('https://api.example.com/users/:id', { id: '$&' })).toBe(
88+
'https://api.example.com/users/%24%26'
89+
)
90+
expect(processUrl('https://api.example.com/users/:user.name', { 'user.name': 'ada' })).toBe(
91+
'https://api.example.com/users/ada'
92+
)
93+
expect(processUrl('https://api.example.com/v1/:$ref', { $ref: 'x' })).toBe(
94+
'https://api.example.com/v1/x'
95+
)
96+
expect(processUrl('https://api.example.com/users/:id', { '/': 'x', '1': 'y' })).toBe(
97+
'https://api.example.com/users/:id'
98+
)
99+
})
100+
73101
it.concurrent('canonicalizes first-party API calls before the apex redirect', () => {
74102
expect(
75103
processUrl('https://sim.ai/api/v2/workflows', undefined, [

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { escapeRegExp } from '@/executor/constants'
12
import { transformTable } from '@/tools/shared/table'
23
import type { TableRow } from '@/tools/types'
34

@@ -81,6 +82,31 @@ export const getDefaultHeaders = (
8182
return headers
8283
}
8384

85+
/**
86+
* A path parameter key must start like a JavaScript identifier, the way `:name` placeholders are
87+
* defined by path-to-regexp. That excludes an empty key and one starting with a digit or `/`, the
88+
* shapes that matched the scheme separator or a port; the rest of the key is left as callers use it.
89+
*/
90+
const PATH_PARAM_KEY = /^[A-Za-z_$][^\s/?#]*$/
91+
92+
/**
93+
* Replaces the first `:key` placeholder for each path parameter with its URL-encoded value.
94+
*
95+
* A placeholder ends where an identifier would, so `:id` never matches inside `:idx`. A plain
96+
* string replace let an empty key strip the scheme's colon (`https://` became `https//`), a
97+
* numeric key rewrite a port, and `:id` match inside `:idx`.
98+
*/
99+
function substitutePathParams(url: string, pathParams: Record<string, string>): string {
100+
let substituted = url
101+
for (const [key, value] of Object.entries(pathParams)) {
102+
if (!PATH_PARAM_KEY.test(key)) continue
103+
substituted = substituted.replace(new RegExp(`:${escapeRegExp(key)}(?![\\w$])`), () =>
104+
encodeURIComponent(value)
105+
)
106+
}
107+
return substituted
108+
}
109+
84110
/**
85111
* Processes a URL with path parameters and query parameters
86112
* @param url Base URL to process
@@ -98,9 +124,7 @@ export const processUrl = (
98124
}
99125

100126
if (pathParams) {
101-
Object.entries(pathParams).forEach(([key, value]) => {
102-
url = url.replace(`:${key}`, encodeURIComponent(value))
103-
})
127+
url = substitutePathParams(url, pathParams)
104128
}
105129

106130
if (queryParams) {

0 commit comments

Comments
 (0)