From 65fcb1ddd43ea3376501c5da11daa97c43ff6db2 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:38:17 +0530 Subject: [PATCH] fix: fixed url encode issue for special symbols --- .talismanrc | 2 +- CHANGELOG.md | 14 ++++++-------- package-lock.json | 31 +++++++++---------------------- package.json | 4 ++-- src/lib/param-serializer.ts | 2 +- src/lib/request.ts | 26 ++++++++++---------------- test/param-serializer.spec.ts | 18 ++++++++++++++++-- test/request.spec.ts | 28 ++++++++++++++-------------- 8 files changed, 59 insertions(+), 66 deletions(-) diff --git a/.talismanrc b/.talismanrc index 5d4c472..bbdd219 100644 --- a/.talismanrc +++ b/.talismanrc @@ -3,7 +3,7 @@ fileignoreconfig: ignore_detectors: - filecontent - filename: package-lock.json - checksum: 3f095735d07bd662952f037664e7ac61ce7841b5940ab16af4a3ef4ad9076d13 + checksum: 4a47373a7a9548e1feb6cd50157f7dae495066fc959908d79b961be7da32cf42 - filename: .husky/pre-commit checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193 - filename: test/request.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 41bafda..a5a2742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,12 @@ ## Change log -### Version: 1.3.8 -#### Date: Jan-12-2026 - - Fix: Add .js extensions to relative imports in ESM build for proper module resolution - - Fix: Change lodash import from named import to default import for ESM compatibility with CommonJS modules - -### Version: 1.3.7 -#### Date: Jan-12-2026 - - Fix: Improve error messages +### Version: 1.3.10 +#### Date: Feb-13-2026 + - Fix: fix url encode for special symbols. +### Version: 1.3.9 +#### Date: Jan-28-2026 + - Fix: Resolve lodash dependency snyk issue ### Version: 1.3.8 #### Date: Jan-15-2026 diff --git a/package-lock.json b/package-lock.json index f8dd5f5..05a224d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@contentstack/core", - "version": "1.3.9", + "version": "1.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/core", - "version": "1.3.9", + "version": "1.3.10", "license": "MIT", "dependencies": { - "axios": "^1.13.4", + "axios": "^1.13.5", "axios-mock-adapter": "^2.1.0", "lodash": "^4.17.23", "qs": "6.14.1", @@ -2209,8 +2209,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", @@ -2332,7 +2331,6 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -2749,7 +2747,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2833,7 +2830,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3226,14 +3222,13 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", - "peer": true, "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -3460,7 +3455,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5115,7 +5109,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5423,7 +5416,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7760,7 +7752,6 @@ "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^28.1.3", "@jest/types": "^28.1.3", @@ -9747,7 +9738,6 @@ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -11500,7 +11490,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11747,7 +11736,6 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11988,7 +11976,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index 1d6b5d9..be66e8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/core", - "version": "1.3.9", + "version": "1.3.10", "type": "commonjs", "main": "./dist/cjs/src/index.js", "types": "./dist/cjs/src/index.d.ts", @@ -33,7 +33,7 @@ "husky-check": "npx husky install && chmod +x .husky/pre-commit" }, "dependencies": { - "axios": "^1.13.4", + "axios": "^1.13.5", "axios-mock-adapter": "^2.1.0", "lodash": "^4.17.23", "qs": "6.14.1", diff --git a/src/lib/param-serializer.ts b/src/lib/param-serializer.ts index c7592eb..0c467ec 100644 --- a/src/lib/param-serializer.ts +++ b/src/lib/param-serializer.ts @@ -5,7 +5,7 @@ export function serialize(params: Record) { delete params.query; let qs = Qs.stringify(params, { arrayFormat: 'brackets' }); if (query) { - qs = qs + `&query=${encodeURI(JSON.stringify(query))}`; + qs = qs + `&query=${encodeURIComponent(JSON.stringify(query))}`; } params.query = query; diff --git a/src/lib/request.ts b/src/lib/request.ts index 096056f..8b3c3f4 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -3,26 +3,19 @@ import { serialize } from './param-serializer'; import { APIError } from './api-error'; import { ERROR_MESSAGES } from './error-messages'; -/** - * Handles array parameters properly with & separators - * React Native compatible implementation without URLSearchParams.set() - */ -function serializeParams(params: any): string { - if (!params) return ''; - - return serialize(params); -} - /** * Builds the full URL with query parameters + * Used only for long URLs that need to be passed directly to axios */ -function buildFullUrl(baseURL: string | undefined, url: string, queryString: string): string { +function buildFullUrl(baseURL: string | undefined, url: string, params: any): string { + const queryString = params ? serialize(params) : ''; + if (url.startsWith('http://') || url.startsWith('https://')) { - return `${url}?${queryString}`; + return queryString ? `${url}?${queryString}` : url; } const base = baseURL || ''; - return `${base}${url}?${queryString}`; + return queryString ? `${base}${url}?${queryString}` : `${base}${url}`; } /** @@ -58,7 +51,7 @@ async function makeRequest( // Determine URL length threshold based on whether it's a preview endpoint // rest-preview.contentstack.com has stricter limits, so use lower threshold const isPreview = isPreviewEndpoint(actualFullUrl); - const urlLengthThreshold = isPreview ? 1500 : 2000; + const urlLengthThreshold = isPreview ? 1500 : 8000; // Increased from 2000 to 8000 // If URL is too long, use direct axios request with full URL if (actualFullUrl.length > urlLengthThreshold) { @@ -117,8 +110,9 @@ export async function getData(instance: AxiosInstance, url: string, data?: any) maxContentLength: Infinity, maxBodyLength: Infinity, }; - const queryString = serializeParams(requestConfig.params); - const actualFullUrl = buildFullUrl(instance.defaults.baseURL, url, queryString); + + // Build full URL with serialized params (only used for long URLs) + const actualFullUrl = buildFullUrl(instance.defaults.baseURL, url, requestConfig.params); const response = await makeRequest(instance, url, requestConfig, actualFullUrl); if (response?.data) { diff --git a/test/param-serializer.spec.ts b/test/param-serializer.spec.ts index e660216..f6ca1ab 100644 --- a/test/param-serializer.spec.ts +++ b/test/param-serializer.spec.ts @@ -21,15 +21,29 @@ describe('serialize', () => { it('should return query with encode string when passed query param', (done) => { const param = serialize({ query: { title: { $in: ['welcome', 'hello'] } } }); - expect(param).toEqual('&query=%7B%22title%22:%7B%22$in%22:%5B%22welcome%22,%22hello%22%5D%7D%7D'); + expect(param).toEqual('&query=%7B%22title%22%3A%7B%22%24in%22%3A%5B%22welcome%22%2C%22hello%22%5D%7D%7D'); done(); }); it('should return brackets and query with encoded string when passed query param and array value', (done) => { const param = serialize({ include: ['reference'], query: { title: { $in: ['welcome', 'hello'] } } }); expect(param).toEqual( - 'include%5B%5D=reference&query=%7B%22title%22:%7B%22$in%22:%5B%22welcome%22,%22hello%22%5D%7D%7D' + 'include%5B%5D=reference&query=%7B%22title%22%3A%7B%22%24in%22%3A%5B%22welcome%22%2C%22hello%22%5D%7D%7D' ); done(); }); + + it('should properly encode special characters like ampersand in query values', (done) => { + const param = serialize({ query: { url: '/imaging-&-automation' } }); + // The & should be encoded as %26 + expect(param).toEqual('&query=%7B%22url%22%3A%22%2Fimaging-%26-automation%22%7D'); + done(); + }); + + it('should properly encode other URL-special characters in query values', (done) => { + const param = serialize({ query: { title: 'test=value&foo=bar?baz' } }); + // =, &, and ? should all be encoded + expect(param).toEqual('&query=%7B%22title%22%3A%22test%3Dvalue%26foo%3Dbar%3Fbaz%22%7D'); + done(); + }); }); diff --git a/test/request.spec.ts b/test/request.spec.ts index c41f6c7..3fca0b7 100644 --- a/test/request.spec.ts +++ b/test/request.spec.ts @@ -346,15 +346,15 @@ describe('Request tests', () => { expect(result).toEqual(mockResponse); }); - it('should use instance.request when URL length exceeds 2000 characters', async () => { + it('should use instance.request when URL length exceeds 8000 characters', async () => { const client = httpClient({ defaultHostname: 'example.com' }); const url = '/your-api-endpoint'; const mockResponse = { data: 'mocked' }; - // Create a very long query parameter that will exceed 2000 characters when combined with baseURL + // Create a very long query parameter that will exceed 8000 characters when combined with baseURL // baseURL is typically like 'https://example.com:443/v3' (~30 chars), url is '/your-api-endpoint' (~20 chars) - // So we need params that serialize to >1950 chars to exceed 2000 total - const longParam = 'x'.repeat(2000); + // So we need params that serialize to >7950 chars to exceed 8000 total + const longParam = 'x'.repeat(8000); const requestData = { params: { longParam, param2: 'y'.repeat(500) } }; // Mock instance.request since that's what gets called for long URLs @@ -498,15 +498,15 @@ describe('Request tests', () => { expect(mock.history.get[0].url).toBe(absoluteUrl); }); - it('should handle absolute URL when actualFullUrl exceeds 2000 characters', async () => { + it('should handle absolute URL when actualFullUrl exceeds 8000 characters', async () => { const client = httpClient({ defaultHostname: 'example.com', }); const absoluteUrl = 'https://external-api.com/api/endpoint'; const mockResponse = { data: 'mocked' }; - // Create a very long query parameter that will exceed 2000 characters - const longParam = 'x'.repeat(2000); + // Create a very long query parameter that will exceed 8000 characters + const longParam = 'x'.repeat(8000); const requestData = { params: { longParam, param2: 'y'.repeat(500) } }; // Mock instance.request since that's what gets called for long URLs @@ -542,8 +542,8 @@ describe('Request tests', () => { const absoluteUrl = 'https://external-api.com/api/endpoint'; const mockResponse = { data: 'mocked' }; - // Create params that will make URL exceed 2000 characters - const longParam = 'x'.repeat(2000); + // Create params that will make URL exceed 8000 characters + const longParam = 'x'.repeat(8000); const requestData = { params: { longParam, param2: 'y'.repeat(500) } }; // Mock instance.request since URL will exceed threshold @@ -576,7 +576,7 @@ describe('Request tests', () => { }; // Create include[] parameters that would exceed 1500 chars for Live Preview - // but would be under 2000 chars for regular requests + // but would be under 8000 chars for regular requests const manyIncludes = Array.from({ length: 100 }, (_, i) => `ref_field_${i}_with_long_name`); const requestData = { params: { 'include[]': manyIncludes } }; @@ -622,16 +622,16 @@ describe('Request tests', () => { expect(mock.history.get.length).toBe(1); }); - it('should use instance.request for regular URLs exceeding 2000 characters', async () => { + it('should use instance.request for regular URLs exceeding 8000 characters', async () => { const client = httpClient({ defaultHostname: 'example.com' }); const url = '/content_types/blog/entries/entry123'; const mockResponse = { entry: { uid: 'entry123', title: 'Test' } }; - // Create many include[] parameters that will exceed 2000 characters - const manyIncludes = Array.from({ length: 200 }, (_, i) => `ref_field_${i}_with_very_long_name_to_make_url_long`); + // Create many include[] parameters that will exceed 8000 characters + const manyIncludes = Array.from({ length: 500 }, (_, i) => `ref_field_${i}_with_very_long_name_to_make_url_long`); const requestData = { params: { 'include[]': manyIncludes } }; - // Mock instance.request since URL will exceed 2000 chars + // Mock instance.request since URL will exceed 8000 chars const requestSpy = jest.spyOn(client, 'request').mockResolvedValue({ data: mockResponse } as any); const result = await getData(client, url, requestData);