Skip to content

Commit 11aee76

Browse files
authored
Merge pull request #179 from contentstack/fix/DX-3851
feat: add url length estimation and compact format support for parame…
2 parents 309feb3 + fd35a35 commit 11aee76

File tree

8 files changed

+260
-1923
lines changed

8 files changed

+260
-1923
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Change log
22

3+
### Version: 1.3.9
4+
#### Date: Jan-26-2026
5+
- Fix: add url length estimation and compact format support for parameter serialization
6+
37
### Version: 1.3.8
48
#### Date: Jan-12-2026
59
- Fix: Add .js extensions to relative imports in ESM build for proper module resolution

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/core",
3-
"version": "1.3.8",
3+
"version": "1.3.9",
44
"type": "commonjs",
55
"main": "./dist/cjs/src/index.js",
66
"types": "./dist/cjs/src/index.d.ts",

src/lib/error-messages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export const ERROR_MESSAGES = {
2525
// Request Handler Messages
2626
REQUEST: {
2727
HOST_REQUIRED_FOR_LIVE_PREVIEW: 'Host is required for live preview. Please provide a valid host parameter in the live_preview configuration.',
28+
URL_TOO_LONG: (urlLength: number, maxLength: number) =>
29+
`Request URL length (${urlLength} characters) exceeds the maximum allowed length (${maxLength} characters). ` +
30+
'Please reduce the number of includeReference parameters or split your request into multiple smaller requests.',
2831
},
2932

3033
// Retry Policy Messages

src/lib/param-serializer.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import * as Qs from 'qs';
2+
import { ParamsSerializerOptions } from 'axios';
3+
4+
interface ExtendedParamsSerializerOptions extends ParamsSerializerOptions {
5+
useCompactFormat?: boolean;
6+
}
7+
8+
export function serialize(
9+
params: Record<string, any>,
10+
options?: ParamsSerializerOptions | ExtendedParamsSerializerOptions | boolean
11+
): string {
12+
// Support both axios signature (options object) and legacy signature (boolean)
13+
const useCompactFormat =
14+
typeof options === 'boolean' ? options : (options as ExtendedParamsSerializerOptions)?.useCompactFormat ?? false;
215

3-
export function serialize(params: Record<string, any>) {
416
const query = params.query;
517
delete params.query;
6-
let qs = Qs.stringify(params, { arrayFormat: 'brackets' });
18+
const arrayFormat = useCompactFormat ? 'comma' : 'brackets';
19+
let qs = Qs.stringify(params, { arrayFormat });
720
if (query) {
821
qs = qs + `&query=${encodeURI(JSON.stringify(query))}`;
922
}

src/lib/request.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,33 @@ import { ERROR_MESSAGES } from './error-messages';
77
* Handles array parameters properly with & separators
88
* React Native compatible implementation without URLSearchParams.set()
99
*/
10-
function serializeParams(params: any): string {
10+
function serializeParams(params: any, useCompactFormat = false): string {
1111
if (!params) return '';
1212

13-
return serialize(params);
13+
return serialize(params, { useCompactFormat } as any);
14+
}
15+
16+
/**
17+
* Estimates the URL length that would be generated from the given parameters
18+
* @param baseURL - The base URL
19+
* @param url - The endpoint URL
20+
* @param params - The query parameters
21+
* @param useCompactFormat - Whether to use compact format for estimation
22+
* @returns Estimated URL length
23+
*/
24+
function estimateUrlLength(baseURL: string | undefined, url: string, params: any, useCompactFormat = false): number {
25+
if (!params) {
26+
const base = baseURL || '';
27+
28+
return (url.startsWith('http://') || url.startsWith('https://') ? url : `${base}${url}`).length;
29+
}
30+
31+
const queryString = serializeParams(params, useCompactFormat);
32+
const base = baseURL || '';
33+
const fullUrl =
34+
url.startsWith('http://') || url.startsWith('https://') ? `${url}?${queryString}` : `${base}${url}?${queryString}`;
35+
36+
return fullUrl.length;
1437
}
1538

1639
/**
@@ -59,6 +82,13 @@ function handleRequestError(err: any): Error {
5982

6083
export async function getData(instance: AxiosInstance, url: string, data?: any) {
6184
try {
85+
let isLivePreview = false;
86+
let livePreviewUrl = url;
87+
88+
if (!data) {
89+
data = {};
90+
}
91+
6292
if (instance.stackConfig && instance.stackConfig.live_preview) {
6393
const livePreviewParams = instance.stackConfig.live_preview;
6494

@@ -76,7 +106,9 @@ export async function getData(instance: AxiosInstance, url: string, data?: any)
76106
if (!livePreviewParams.host) {
77107
throw new Error(ERROR_MESSAGES.REQUEST.HOST_REQUIRED_FOR_LIVE_PREVIEW);
78108
}
79-
url = (livePreviewParams.host.startsWith('https://') ? '' : 'https://') + livePreviewParams.host + url;
109+
livePreviewUrl =
110+
(livePreviewParams.host.startsWith('https://') ? '' : 'https://') + livePreviewParams.host + url;
111+
isLivePreview = true;
80112
}
81113
}
82114
}
@@ -85,10 +117,41 @@ export async function getData(instance: AxiosInstance, url: string, data?: any)
85117
...data,
86118
maxContentLength: Infinity,
87119
maxBodyLength: Infinity,
88-
};
89-
const queryString = serializeParams(requestConfig.params);
90-
const actualFullUrl = buildFullUrl(instance.defaults.baseURL, url, queryString);
91-
const response = await makeRequest(instance, url, requestConfig, actualFullUrl);
120+
} as any;
121+
122+
// Determine URL length thresholds
123+
// Use lower threshold for Live Preview (1500) vs regular requests (2000)
124+
const maxUrlLength = isLivePreview ? 1500 : 2000;
125+
const baseURLForEstimation = isLivePreview ? undefined : instance.defaults.baseURL;
126+
const urlForEstimation = isLivePreview ? livePreviewUrl : url;
127+
128+
// Estimate URL length with standard format
129+
const estimatedLength = estimateUrlLength(baseURLForEstimation, urlForEstimation, requestConfig.params, false);
130+
let useCompactFormat = false;
131+
132+
// If URL would exceed threshold, try compact format
133+
if (estimatedLength > maxUrlLength) {
134+
const compactEstimatedLength = estimateUrlLength(
135+
baseURLForEstimation,
136+
urlForEstimation,
137+
requestConfig.params,
138+
true
139+
);
140+
if (compactEstimatedLength <= maxUrlLength) {
141+
useCompactFormat = true;
142+
} else {
143+
// Even with compact format, URL is too long
144+
throw new Error(ERROR_MESSAGES.REQUEST.URL_TOO_LONG(compactEstimatedLength, maxUrlLength));
145+
}
146+
}
147+
148+
const queryString = serializeParams(requestConfig.params, useCompactFormat);
149+
const actualFullUrl = buildFullUrl(
150+
isLivePreview ? undefined : instance.defaults.baseURL,
151+
isLivePreview ? livePreviewUrl : url,
152+
queryString
153+
);
154+
const response = await makeRequest(instance, isLivePreview ? livePreviewUrl : url, requestConfig, actualFullUrl);
92155

93156
if (response && response.data) {
94157
return response.data;

test/param-serializer.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,23 @@ describe('serialize', () => {
3232
);
3333
done();
3434
});
35+
36+
it('should return comma-separated format when useCompactFormat is true', (done) => {
37+
const param = serialize({ 'include[]': ['ref1', 'ref2', 'ref3'] }, true);
38+
expect(param).toEqual('include=ref1%2Cref2%2Cref3');
39+
done();
40+
});
41+
42+
it('should return brackets format when useCompactFormat is false', (done) => {
43+
const param = serialize({ 'include[]': ['ref1', 'ref2', 'ref3'] }, false);
44+
expect(param).toEqual('include%5B%5D=ref1&include%5B%5D=ref2&include%5B%5D=ref3');
45+
done();
46+
});
47+
48+
it('should handle query param with compact format', (done) => {
49+
const param = serialize({ 'include[]': ['ref1', 'ref2'], query: { title: 'test' } }, true);
50+
expect(param).toContain('include=ref1%2Cref2');
51+
expect(param).toContain('query=');
52+
done();
53+
});
3554
});

test/request.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,4 +529,98 @@ describe('Request tests', () => {
529529
requestSpy.mockRestore();
530530
});
531531
});
532+
533+
describe('URL length optimization for includeReference parameters', () => {
534+
it('should use compact format when URL with many include[] parameters exceeds threshold', async () => {
535+
const client = httpClient({ defaultHostname: 'example.com' });
536+
const mock = new MockAdapter(client as any);
537+
const url = '/content_types/blog/entries/entry123';
538+
const mockResponse = { entry: { uid: 'entry123', title: 'Test' } };
539+
540+
// Create many include[] parameters that would make URL long
541+
const manyIncludes = Array.from({ length: 100 }, (_, i) => `ref_field_${i}`);
542+
const requestData = { params: { 'include[]': manyIncludes } };
543+
544+
mock.onGet(url).reply(200, mockResponse);
545+
546+
const result = await getData(client, url, requestData);
547+
expect(result).toEqual(mockResponse);
548+
549+
// Verify the request was made (URL optimization allowed it to succeed)
550+
expect(mock.history.get.length).toBe(1);
551+
const requestUrl = mock.history.get[0].url || '';
552+
// With compact format, the URL should be shorter and contain comma-separated values
553+
// We verify success means the optimization worked
554+
expect(requestUrl.length).toBeLessThan(3000);
555+
});
556+
557+
it('should use compact format for Live Preview requests with lower threshold', async () => {
558+
const client = httpClient({ defaultHostname: 'example.com' });
559+
const mock = new MockAdapter(client as any);
560+
const url = '/content_types/blog/entries/entry123';
561+
const mockResponse = { entry: { uid: 'entry123', title: 'Test' } };
562+
563+
client.stackConfig = {
564+
live_preview: {
565+
enable: true,
566+
preview_token: 'someToken',
567+
live_preview: 'someHash',
568+
host: 'rest-preview.contentstack.com',
569+
},
570+
};
571+
572+
// Create include[] parameters that would exceed 1500 chars for Live Preview
573+
// but might be okay for regular requests (2000 chars)
574+
const manyIncludes = Array.from({ length: 80 }, (_, i) => `ref_field_${i}_with_long_name`);
575+
const requestData = { params: { 'include[]': manyIncludes } };
576+
577+
const livePreviewUrl = 'https://rest-preview.contentstack.com' + url;
578+
mock.onGet(livePreviewUrl).reply(200, mockResponse);
579+
580+
const result = await getData(client, url, requestData);
581+
expect(result).toEqual(mockResponse);
582+
583+
// Verify the request was made to Live Preview host
584+
expect(mock.history.get.length).toBe(1);
585+
expect(mock.history.get[0].url).toContain('rest-preview.contentstack.com');
586+
});
587+
588+
it('should throw error when URL is too long even with compact format', async () => {
589+
const client = httpClient({ defaultHostname: 'example.com' });
590+
const url = '/content_types/blog/entries/entry123';
591+
592+
client.stackConfig = {
593+
live_preview: {
594+
enable: true,
595+
preview_token: 'someToken',
596+
live_preview: 'someHash',
597+
host: 'rest-preview.contentstack.com',
598+
},
599+
};
600+
601+
// Create an extremely large number of includes that would exceed even compact format
602+
const manyIncludes = Array.from({ length: 500 }, (_, i) => `very_long_reference_field_name_${i}_with_many_characters`);
603+
const requestData = { params: { 'include[]': manyIncludes } };
604+
605+
await expect(getData(client, url, requestData)).rejects.toThrow(/exceeds the maximum allowed length/);
606+
});
607+
608+
it('should use standard format when URL length is within threshold', async () => {
609+
const client = httpClient({ defaultHostname: 'example.com' });
610+
const mock = new MockAdapter(client as any);
611+
const url = '/content_types/blog/entries/entry123';
612+
const mockResponse = { entry: { uid: 'entry123', title: 'Test' } };
613+
614+
// Create a small number of includes that won't exceed threshold
615+
const requestData = { params: { 'include[]': ['ref1', 'ref2', 'ref3'] } };
616+
617+
mock.onGet(url).reply(200, mockResponse);
618+
619+
const result = await getData(client, url, requestData);
620+
expect(result).toEqual(mockResponse);
621+
622+
// Verify the request was made successfully
623+
expect(mock.history.get.length).toBe(1);
624+
});
625+
});
532626
});

0 commit comments

Comments
 (0)