Skip to content

Commit 427d7ad

Browse files
Fix: Custom API endpoint incorrect path encoding (@W-20091358@) - Hotfix only (#248)
* implement fix * bump size * address pr comments * remove multiple slashes
1 parent 36afdb4 commit 427d7ad

File tree

4 files changed

+126
-4
lines changed

4 files changed

+126
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
## v4.1.0
44

5-
### Enchancements
5+
### Enhancements
66

77
- Use native node fetch available in node 18+ instead of `node-fetch` polyfill [#214](https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/pull/214)
88
- Support subpath imports for individual APIs and named imports [#219](https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/pull/219)
99

10+
### Bug Fixes
11+
12+
- Fix incorrect encoding of multi-segment endpoint paths in `callCustomEndpoint` [#246](https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/pull/246)
13+
1014
## v4.0.0
1115

1216
### API Versions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@
246246
"bundlesize": [
247247
{
248248
"path": "lib/**/*.js",
249-
"maxSize": "54 kB"
249+
"maxSize": "55 kB"
250250
},
251251
{
252252
"path": "commerce-sdk-isomorphic-with-deps.tgz",

src/static/helpers/customApi.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,96 @@ describe('callCustomEndpoint', () => {
249249
undefined
250250
);
251251
});
252+
253+
test('should support multi-segment paths even with special characters', async () => {
254+
const {shortCode, organizationId} = clientConfig.parameters;
255+
const {apiName} = options.customApiPathParameters;
256+
const endpointPath = 'multi/segment/path/Special,Summer%';
257+
const expectedEndpointPath = 'multi/segment/path/Special%2CSummer%25';
258+
259+
const nockBasePath = `https://${shortCode}.api.commercecloud.salesforce.com`;
260+
const nockEndpointPath = `/custom/${apiName}/v2/organizations/${
261+
organizationId as string
262+
}/${expectedEndpointPath}`;
263+
nock(nockBasePath).post(nockEndpointPath).query(true).reply(200);
264+
265+
const copyOptions = {
266+
...options,
267+
customApiPathParameters: {
268+
...options.customApiPathParameters,
269+
endpointPath,
270+
},
271+
};
272+
273+
const expectedUrl = `${
274+
nockBasePath + nockEndpointPath
275+
}?${queryParamString}`;
276+
const expectedOptions = addSiteIdToOptions(copyOptions);
277+
278+
const expectedClientConfig = {
279+
...clientConfig,
280+
baseUri:
281+
'https://{shortCode}.api.commercecloud.salesforce.com/custom/{apiName}/{apiVersion}',
282+
};
283+
284+
const doFetchSpy = jest.spyOn(fetchHelper, 'doFetch');
285+
await callCustomEndpoint({
286+
options: copyOptions,
287+
clientConfig,
288+
rawResponse: true,
289+
});
290+
expect(doFetchSpy).toBeCalledTimes(1);
291+
expect(doFetchSpy).toBeCalledWith(
292+
expectedUrl,
293+
expectedOptions,
294+
expectedClientConfig,
295+
true
296+
);
297+
});
298+
299+
test('should normalize endpoint path with multiple slashes', async () => {
300+
const {shortCode, organizationId} = clientConfig.parameters;
301+
const {apiName} = options.customApiPathParameters;
302+
const endpointPath = 'multi/segment///path////Special,Summer%';
303+
const expectedEndpointPath = 'multi/segment/path/Special%2CSummer%25';
304+
305+
const nockBasePath = `https://${shortCode}.api.commercecloud.salesforce.com`;
306+
const nockEndpointPath = `/custom/${apiName}/v2/organizations/${
307+
organizationId as string
308+
}/${expectedEndpointPath}`;
309+
nock(nockBasePath).post(nockEndpointPath).query(true).reply(200);
310+
311+
const copyOptions = {
312+
...options,
313+
customApiPathParameters: {
314+
...options.customApiPathParameters,
315+
endpointPath,
316+
},
317+
};
318+
319+
const expectedUrl = `${
320+
nockBasePath + nockEndpointPath
321+
}?${queryParamString}`;
322+
const expectedOptions = addSiteIdToOptions(copyOptions);
323+
324+
const expectedClientConfig = {
325+
...clientConfig,
326+
baseUri:
327+
'https://{shortCode}.api.commercecloud.salesforce.com/custom/{apiName}/{apiVersion}',
328+
};
329+
330+
const doFetchSpy = jest.spyOn(fetchHelper, 'doFetch');
331+
await callCustomEndpoint({
332+
options: copyOptions,
333+
clientConfig,
334+
rawResponse: true,
335+
});
336+
expect(doFetchSpy).toBeCalledTimes(1);
337+
expect(doFetchSpy).toBeCalledWith(
338+
expectedUrl,
339+
expectedOptions,
340+
expectedClientConfig,
341+
true
342+
);
343+
});
252344
});

src/static/helpers/customApi.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,37 @@ export const callCustomEndpoint = async (args: {
134134
};
135135
}
136136

137+
const currentEndpointPath = (options?.customApiPathParameters?.endpointPath ||
138+
clientConfig.parameters?.endpointPath) as string;
139+
let newEndpointPath = currentEndpointPath;
140+
const endpointPathSegments = {} as Record<string, string>;
141+
142+
// Normalize and template the endpointPath so each segment is encoded as a path param.
143+
// Example:
144+
// currentEndpointPath: "action/categories/Special,Summer" ->
145+
// endpointPathParams: { endpointPathSegment0: "action", endpointPathSegment1: "categories", endpointPathSegment2: "Special,Summer" }
146+
// newEndpointPath: "{endpointPathSegment0}/{endpointPathSegment1}/{endpointPathSegment2}/"
147+
// The TemplateURL will then encode the path parameters and construct the URL with the encoded path parameters
148+
// The resulting endpointPath will be: "actions/categories/Special%2CSummer"
149+
if (currentEndpointPath.includes('/')) {
150+
// Normalize endpoint path by removing multiple consecutive slashes
151+
const segments = currentEndpointPath.split('/').filter(segment => segment !== '');
152+
newEndpointPath = '';
153+
segments.forEach((segment: string, index: number) => {
154+
const key = `endpointPathSegment${index}`;
155+
endpointPathSegments[key] = segment;
156+
newEndpointPath += `{${key}}/`;
157+
});
158+
// Remove the trailing slash added after the last segment
159+
// as TemplateURL does not expect a trailing slash
160+
newEndpointPath = newEndpointPath.slice(0, -1);
161+
}
162+
137163
const url = new TemplateURL(
138-
'/organizations/{organizationId}/{endpointPath}',
164+
`/organizations/{organizationId}/${newEndpointPath}`,
139165
clientConfigCopy.baseUri as string,
140166
{
141-
pathParams: pathParams as PathParameters,
167+
pathParams: {...pathParams, ...endpointPathSegments} as PathParameters,
142168
queryParams: optionsCopy.parameters,
143169
origin: clientConfigCopy.proxy,
144170
}

0 commit comments

Comments
 (0)