diff --git a/CHANGELOG.md b/CHANGELOG.md index b0247b7..f81d377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### v3.28.1 (2026-07-16) +* * * +### Bug Fixes: +- Stopped setting the `Content-Length` header manually in `RequestWrapper`. `fetch`/undici already derives the correct value from the request `body`, so the manual header was redundant. When `FetchHttpClient` re-wraps the request (`new Request(request, { signal })`), some Node builds re-append the body-derived value, producing a comma-joined `"N, N"`. undici (>= 7.28) validates `Content-Length` with a strict all-digit check and rejects that value with `InvalidArgumentError: invalid content-length header`, failing the request before it reaches Chargebee. The header is now left to the platform. This is distinct from the value-correctness fix in v3.24.1 (issue #119). Verified on Node 18/20/22/24 and Cloudflare Workers (`workerd`), where the runtime emits the correct UTF-8 byte-length `Content-Length` on the wire. + +### Tests: +- POST requests no longer set a `Content-Length` header on the outgoing `Request` (ASCII form-urlencoded and multi-byte JSON bodies), guarding against the duplicate-header regression while preserving body integrity. + + ### v3.28.0 (2026-06-30) * * * diff --git a/VERSION b/VERSION index a72fd67..54d1636 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.28.0 +3.28.1 diff --git a/package-lock.json b/package-lock.json index d6ca222..faa7c56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chargebee", - "version": "3.28.0", + "version": "3.28.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chargebee", - "version": "3.28.0", + "version": "3.28.1", "dependencies": { "zod": "^4.3.6" }, diff --git a/package.json b/package.json index 06a4647..f1e1cda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chargebee", - "version": "3.28.0", + "version": "3.28.1", "description": "A library for integrating with Chargebee.", "scripts": { "prepack": "npm install && npm run build", diff --git a/src/RequestWrapper.ts b/src/RequestWrapper.ts index df3b0ae..988d109 100644 --- a/src/RequestWrapper.ts +++ b/src/RequestWrapper.ts @@ -233,11 +233,6 @@ export class RequestWrapper { ...this.httpHeaders, ...telemetryHeaders, }; - if (data && data.length) { - extend(true, requestHeaders, { - 'Content-Length': Buffer.byteLength(data, 'utf8'), - }); - } const contentType = this.apiCall.isJsonRequest ? 'application/json;charset=UTF-8' diff --git a/src/environment.ts b/src/environment.ts index a57f3db..78b92db 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -9,7 +9,7 @@ export const Environment = { hostSuffix: '.chargebee.com', apiPath: '/api/v2', timeout: DEFAULT_TIME_OUT, - clientVersion: 'v3.28.0', + clientVersion: 'v3.28.1', port: DEFAULT_PORT, timemachineWaitInMillis: DEFAULT_TIME_MACHINE_WAIT, exportWaitInMillis: DEFAULT_EXPORT_WAIT, diff --git a/test/requestWrapper.test.ts b/test/requestWrapper.test.ts index b0f9494..45c30e3 100644 --- a/test/requestWrapper.test.ts +++ b/test/requestWrapper.test.ts @@ -191,7 +191,12 @@ describe('RequestWrapper - request headers', () => { }); describe('Content-Length header', () => { - it('should set Content-Length to the UTF-8 byte length for ASCII form-urlencoded bodies', async () => { + // The SDK must NOT set Content-Length manually. fetch/undici derives it from + // `body` at dispatch time; a manual header is redundant and can be re-appended + // into a comma-joined "N, N" when FetchHttpClient re-wraps the Request, which + // undici (>= 7.28) rejects as an invalid content-length header. + // See: RequestWrapper#request and src/net/FetchClient.ts. + it('should NOT set Content-Length manually for ASCII form-urlencoded bodies', async () => { responseFactory = () => new Response(JSON.stringify({ customer: { id: 'cust_123' } }), { status: 200, @@ -202,13 +207,11 @@ describe('RequestWrapper - request headers', () => { await chargebee.customer.create({ first_name: 'John' }); const body = await capturedRequests[0].text(); - const expected = Buffer.byteLength(body, 'utf8'); - expect(capturedRequests[0].headers.get('Content-Length')).to.equal( - String(expected), - ); + expect(body).to.include('first_name=John'); + expect(capturedRequests[0].headers.get('Content-Length')).to.be.null; }); - it('should set Content-Length to the UTF-8 byte length (not character count) for multi-byte JSON bodies', async () => { + it('should NOT set Content-Length manually for multi-byte JSON bodies', async () => { responseFactory = () => new Response(JSON.stringify({ personalized_offers: [] }), { status: 200, @@ -225,10 +228,9 @@ describe('RequestWrapper - request headers', () => { const body = await capturedRequests[0].text(); const byteLength = Buffer.byteLength(body, 'utf8'); const charLength = body.length; + // Body integrity: multi-byte payload is preserved intact. expect(byteLength).to.be.greaterThan(charLength); - expect(capturedRequests[0].headers.get('Content-Length')).to.equal( - String(byteLength), - ); + expect(capturedRequests[0].headers.get('Content-Length')).to.be.null; }); });