Skip to content

Commit 65fcb1d

Browse files
fix: fixed url encode issue for special symbols
1 parent 574e51d commit 65fcb1d

File tree

8 files changed

+59
-66
lines changed

8 files changed

+59
-66
lines changed

.talismanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ fileignoreconfig:
33
ignore_detectors:
44
- filecontent
55
- filename: package-lock.json
6-
checksum: 3f095735d07bd662952f037664e7ac61ce7841b5940ab16af4a3ef4ad9076d13
6+
checksum: 4a47373a7a9548e1feb6cd50157f7dae495066fc959908d79b961be7da32cf42
77
- filename: .husky/pre-commit
88
checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193
99
- filename: test/request.spec.ts

CHANGELOG.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
## Change log
22

3-
### Version: 1.3.8
4-
#### Date: Jan-12-2026
5-
- Fix: Add .js extensions to relative imports in ESM build for proper module resolution
6-
- Fix: Change lodash import from named import to default import for ESM compatibility with CommonJS modules
7-
8-
### Version: 1.3.7
9-
#### Date: Jan-12-2026
10-
- Fix: Improve error messages
3+
### Version: 1.3.10
4+
#### Date: Feb-13-2026
5+
- Fix: fix url encode for special symbols.
116

7+
### Version: 1.3.9
8+
#### Date: Jan-28-2026
9+
- Fix: Resolve lodash dependency snyk issue
1210

1311
### Version: 1.3.8
1412
#### Date: Jan-15-2026

package-lock.json

Lines changed: 9 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/core",
3-
"version": "1.3.9",
3+
"version": "1.3.10",
44
"type": "commonjs",
55
"main": "./dist/cjs/src/index.js",
66
"types": "./dist/cjs/src/index.d.ts",
@@ -33,7 +33,7 @@
3333
"husky-check": "npx husky install && chmod +x .husky/pre-commit"
3434
},
3535
"dependencies": {
36-
"axios": "^1.13.4",
36+
"axios": "^1.13.5",
3737
"axios-mock-adapter": "^2.1.0",
3838
"lodash": "^4.17.23",
3939
"qs": "6.14.1",

src/lib/param-serializer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function serialize(params: Record<string, any>) {
55
delete params.query;
66
let qs = Qs.stringify(params, { arrayFormat: 'brackets' });
77
if (query) {
8-
qs = qs + `&query=${encodeURI(JSON.stringify(query))}`;
8+
qs = qs + `&query=${encodeURIComponent(JSON.stringify(query))}`;
99
}
1010
params.query = query;
1111

src/lib/request.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,19 @@ import { serialize } from './param-serializer';
33
import { APIError } from './api-error';
44
import { ERROR_MESSAGES } from './error-messages';
55

6-
/**
7-
* Handles array parameters properly with & separators
8-
* React Native compatible implementation without URLSearchParams.set()
9-
*/
10-
function serializeParams(params: any): string {
11-
if (!params) return '';
12-
13-
return serialize(params);
14-
}
15-
166
/**
177
* Builds the full URL with query parameters
8+
* Used only for long URLs that need to be passed directly to axios
189
*/
19-
function buildFullUrl(baseURL: string | undefined, url: string, queryString: string): string {
10+
function buildFullUrl(baseURL: string | undefined, url: string, params: any): string {
11+
const queryString = params ? serialize(params) : '';
12+
2013
if (url.startsWith('http://') || url.startsWith('https://')) {
21-
return `${url}?${queryString}`;
14+
return queryString ? `${url}?${queryString}` : url;
2215
}
2316
const base = baseURL || '';
2417

25-
return `${base}${url}?${queryString}`;
18+
return queryString ? `${base}${url}?${queryString}` : `${base}${url}`;
2619
}
2720

2821
/**
@@ -58,7 +51,7 @@ async function makeRequest(
5851
// Determine URL length threshold based on whether it's a preview endpoint
5952
// rest-preview.contentstack.com has stricter limits, so use lower threshold
6053
const isPreview = isPreviewEndpoint(actualFullUrl);
61-
const urlLengthThreshold = isPreview ? 1500 : 2000;
54+
const urlLengthThreshold = isPreview ? 1500 : 8000; // Increased from 2000 to 8000
6255

6356
// If URL is too long, use direct axios request with full URL
6457
if (actualFullUrl.length > urlLengthThreshold) {
@@ -117,8 +110,9 @@ export async function getData(instance: AxiosInstance, url: string, data?: any)
117110
maxContentLength: Infinity,
118111
maxBodyLength: Infinity,
119112
};
120-
const queryString = serializeParams(requestConfig.params);
121-
const actualFullUrl = buildFullUrl(instance.defaults.baseURL, url, queryString);
113+
114+
// Build full URL with serialized params (only used for long URLs)
115+
const actualFullUrl = buildFullUrl(instance.defaults.baseURL, url, requestConfig.params);
122116
const response = await makeRequest(instance, url, requestConfig, actualFullUrl);
123117

124118
if (response?.data) {

test/param-serializer.spec.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,29 @@ describe('serialize', () => {
2121

2222
it('should return query with encode string when passed query param', (done) => {
2323
const param = serialize({ query: { title: { $in: ['welcome', 'hello'] } } });
24-
expect(param).toEqual('&query=%7B%22title%22:%7B%22$in%22:%5B%22welcome%22,%22hello%22%5D%7D%7D');
24+
expect(param).toEqual('&query=%7B%22title%22%3A%7B%22%24in%22%3A%5B%22welcome%22%2C%22hello%22%5D%7D%7D');
2525
done();
2626
});
2727

2828
it('should return brackets and query with encoded string when passed query param and array value', (done) => {
2929
const param = serialize({ include: ['reference'], query: { title: { $in: ['welcome', 'hello'] } } });
3030
expect(param).toEqual(
31-
'include%5B%5D=reference&query=%7B%22title%22:%7B%22$in%22:%5B%22welcome%22,%22hello%22%5D%7D%7D'
31+
'include%5B%5D=reference&query=%7B%22title%22%3A%7B%22%24in%22%3A%5B%22welcome%22%2C%22hello%22%5D%7D%7D'
3232
);
3333
done();
3434
});
35+
36+
it('should properly encode special characters like ampersand in query values', (done) => {
37+
const param = serialize({ query: { url: '/imaging-&-automation' } });
38+
// The & should be encoded as %26
39+
expect(param).toEqual('&query=%7B%22url%22%3A%22%2Fimaging-%26-automation%22%7D');
40+
done();
41+
});
42+
43+
it('should properly encode other URL-special characters in query values', (done) => {
44+
const param = serialize({ query: { title: 'test=value&foo=bar?baz' } });
45+
// =, &, and ? should all be encoded
46+
expect(param).toEqual('&query=%7B%22title%22%3A%22test%3Dvalue%26foo%3Dbar%3Fbaz%22%7D');
47+
done();
48+
});
3549
});

test/request.spec.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -346,15 +346,15 @@ describe('Request tests', () => {
346346
expect(result).toEqual(mockResponse);
347347
});
348348

349-
it('should use instance.request when URL length exceeds 2000 characters', async () => {
349+
it('should use instance.request when URL length exceeds 8000 characters', async () => {
350350
const client = httpClient({ defaultHostname: 'example.com' });
351351
const url = '/your-api-endpoint';
352352
const mockResponse = { data: 'mocked' };
353353

354-
// Create a very long query parameter that will exceed 2000 characters when combined with baseURL
354+
// Create a very long query parameter that will exceed 8000 characters when combined with baseURL
355355
// baseURL is typically like 'https://example.com:443/v3' (~30 chars), url is '/your-api-endpoint' (~20 chars)
356-
// So we need params that serialize to >1950 chars to exceed 2000 total
357-
const longParam = 'x'.repeat(2000);
356+
// So we need params that serialize to >7950 chars to exceed 8000 total
357+
const longParam = 'x'.repeat(8000);
358358
const requestData = { params: { longParam, param2: 'y'.repeat(500) } };
359359

360360
// Mock instance.request since that's what gets called for long URLs
@@ -498,15 +498,15 @@ describe('Request tests', () => {
498498
expect(mock.history.get[0].url).toBe(absoluteUrl);
499499
});
500500

501-
it('should handle absolute URL when actualFullUrl exceeds 2000 characters', async () => {
501+
it('should handle absolute URL when actualFullUrl exceeds 8000 characters', async () => {
502502
const client = httpClient({
503503
defaultHostname: 'example.com',
504504
});
505505
const absoluteUrl = 'https://external-api.com/api/endpoint';
506506
const mockResponse = { data: 'mocked' };
507507

508-
// Create a very long query parameter that will exceed 2000 characters
509-
const longParam = 'x'.repeat(2000);
508+
// Create a very long query parameter that will exceed 8000 characters
509+
const longParam = 'x'.repeat(8000);
510510
const requestData = { params: { longParam, param2: 'y'.repeat(500) } };
511511

512512
// Mock instance.request since that's what gets called for long URLs
@@ -542,8 +542,8 @@ describe('Request tests', () => {
542542
const absoluteUrl = 'https://external-api.com/api/endpoint';
543543
const mockResponse = { data: 'mocked' };
544544

545-
// Create params that will make URL exceed 2000 characters
546-
const longParam = 'x'.repeat(2000);
545+
// Create params that will make URL exceed 8000 characters
546+
const longParam = 'x'.repeat(8000);
547547
const requestData = { params: { longParam, param2: 'y'.repeat(500) } };
548548

549549
// Mock instance.request since URL will exceed threshold
@@ -576,7 +576,7 @@ describe('Request tests', () => {
576576
};
577577

578578
// Create include[] parameters that would exceed 1500 chars for Live Preview
579-
// but would be under 2000 chars for regular requests
579+
// but would be under 8000 chars for regular requests
580580
const manyIncludes = Array.from({ length: 100 }, (_, i) => `ref_field_${i}_with_long_name`);
581581
const requestData = { params: { 'include[]': manyIncludes } };
582582

@@ -622,16 +622,16 @@ describe('Request tests', () => {
622622
expect(mock.history.get.length).toBe(1);
623623
});
624624

625-
it('should use instance.request for regular URLs exceeding 2000 characters', async () => {
625+
it('should use instance.request for regular URLs exceeding 8000 characters', async () => {
626626
const client = httpClient({ defaultHostname: 'example.com' });
627627
const url = '/content_types/blog/entries/entry123';
628628
const mockResponse = { entry: { uid: 'entry123', title: 'Test' } };
629629

630-
// Create many include[] parameters that will exceed 2000 characters
631-
const manyIncludes = Array.from({ length: 200 }, (_, i) => `ref_field_${i}_with_very_long_name_to_make_url_long`);
630+
// Create many include[] parameters that will exceed 8000 characters
631+
const manyIncludes = Array.from({ length: 500 }, (_, i) => `ref_field_${i}_with_very_long_name_to_make_url_long`);
632632
const requestData = { params: { 'include[]': manyIncludes } };
633633

634-
// Mock instance.request since URL will exceed 2000 chars
634+
// Mock instance.request since URL will exceed 8000 chars
635635
const requestSpy = jest.spyOn(client, 'request').mockResolvedValue({ data: mockResponse } as any);
636636

637637
const result = await getData(client, url, requestData);

0 commit comments

Comments
 (0)