From ef59c6675ebec030ca40be41f2760ce1abdff66a Mon Sep 17 00:00:00 2001 From: Zio Wang Date: Tue, 11 Aug 2026 17:34:54 +0800 Subject: [PATCH] fix(modernjs-v3): compute Content-Length in bytes for served bundles --- .../modernjs-v3-content-length-bytes.md | 5 +++ .../src/server/staticMiddleware.spec.ts | 43 +++++++++++++++++++ .../src/server/staticMiddleware.ts | 5 ++- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .changeset/modernjs-v3-content-length-bytes.md diff --git a/.changeset/modernjs-v3-content-length-bytes.md b/.changeset/modernjs-v3-content-length-bytes.md new file mode 100644 index 00000000000..c49459c809b --- /dev/null +++ b/.changeset/modernjs-v3-content-length-bytes.md @@ -0,0 +1,5 @@ +--- +'@module-federation/modern-js-v3': patch +--- + +Fix truncated remote chunks served from `/bundles` by computing `Content-Length` in bytes instead of characters. Chunks containing non-ASCII characters were cut short, causing `SyntaxError` when an SSR consumer evaluated them. diff --git a/packages/modernjs-v3/src/server/staticMiddleware.spec.ts b/packages/modernjs-v3/src/server/staticMiddleware.spec.ts index d37cd778b61..ea576ec9911 100644 --- a/packages/modernjs-v3/src/server/staticMiddleware.spec.ts +++ b/packages/modernjs-v3/src/server/staticMiddleware.spec.ts @@ -193,6 +193,49 @@ describe('staticMiddleware', () => { expect(result).toBe('empty-response'); expect(nextSpy).not.toHaveBeenCalled(); }); + + it('should set Content-Length in bytes for non-ASCII content', async () => { + // Multi-byte characters make byte length differ from string length. + // Using the string length here truncates the response body. + const mockFileContent = 'console.log("中文注释");'; + const mockFileResult = { + content: mockFileContent, + lastModified: Date.now(), + }; + + mockContext.req.path = '/bundles/non-ascii.js'; + (access as any).mockResolvedValue(undefined); + (fileCache.getFile as any).mockResolvedValue(mockFileResult); + + await middleware(mockContext, nextSpy); + + expect(Buffer.byteLength(mockFileContent)).toBeGreaterThan( + mockFileContent.length, + ); + expect(mockContext.header).toHaveBeenCalledWith( + 'Content-Length', + String(Buffer.byteLength(mockFileContent)), + ); + }); + + it('should set Content-Length in bytes for ASCII content', async () => { + const mockFileContent = 'console.log("ascii only");'; + const mockFileResult = { + content: mockFileContent, + lastModified: Date.now(), + }; + + mockContext.req.path = '/bundles/ascii.js'; + (access as any).mockResolvedValue(undefined); + (fileCache.getFile as any).mockResolvedValue(mockFileResult); + + await middleware(mockContext, nextSpy); + + expect(mockContext.header).toHaveBeenCalledWith( + 'Content-Length', + String(mockFileContent.length), + ); + }); }); describe('asset prefix handling', () => { diff --git a/packages/modernjs-v3/src/server/staticMiddleware.ts b/packages/modernjs-v3/src/server/staticMiddleware.ts index fd0b909fc54..37cd175802b 100644 --- a/packages/modernjs-v3/src/server/staticMiddleware.ts +++ b/packages/modernjs-v3/src/server/staticMiddleware.ts @@ -63,7 +63,10 @@ const createStaticMiddleware = (options: { } c.header('Content-Type', 'application/javascript'); - c.header('Content-Length', String(fileResult.content.length)); + // The file is read as a UTF-8 string, so `content.length` is the number of + // characters. Content-Length must be the number of bytes, otherwise any + // chunk containing non-ASCII characters is truncated by the difference. + c.header('Content-Length', String(Buffer.byteLength(fileResult.content))); return c.body(fileResult.content, 200); }; };