From 00adb55edc0ccffa3a458eaddab4d21cc5c81a78 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 6 Nov 2026 19:31:19 +0100 Subject: [PATCH] fix(core): `docusaurus serve` serves a baseUrl with non-ASCII characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browsers send the baseUrl percent-encoded, like "/caf%C3%A9/", so the request never matched the raw "/café/" baseUrl: it redirected in a loop, or crashed with characters outside of Latin-1 in the Location header. Co-Authored-By: Claude Opus 5.5 --- .../serve/__tests__/requestHandler.test.ts | 38 ++++++++++++++----- .../src/commands/serve/requestHandler.ts | 6 ++- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts b/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts index dd94c4ae9f13..d27ac56bb269 100644 --- a/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts +++ b/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts @@ -826,27 +826,47 @@ describe('createServeRequestHandler', () => { // Browsers send the baseUrl percent-encoded: "/caf%C3%A9/" describe('baseUrl with non-ASCII characters', () => { - // TODO fix: serve the baseUrl, "200 index.html" - it('redirects to the baseUrl in a loop', async () => { + it('serves the baseUrl', async () => { await expect( serveRequest( {baseUrl: '/café/', trailingSlash: undefined}, '/caf%C3%A9/', ), - ).resolves.toBe( - '302 /café/ → 302 /café/ → 302 /café/ → 302 /café/ → 302 /café/ → 302 /café/', - ); + ).resolves.toBe('200 index.html'); }); - // TODO fix: serve the baseUrl, "200 index.html" - // Characters outside of Latin-1 are invalid in a Location header - it('crashes with characters outside of Latin-1', async () => { + it('serves the baseUrl with characters outside of Latin-1', async () => { await expect( serveRequest( {baseUrl: '/文档/', trailingSlash: undefined}, '/%E6%96%87%E6%A1%A3/', ), - ).resolves.toBe('crash ERR_INVALID_CHAR'); + ).resolves.toBe('200 index.html'); + }); + + // Characters outside of Latin-1 are invalid in a Location header + it('redirects to the percent-encoded baseUrl', async () => { + await expect( + serveRequest({baseUrl: '/文档/', trailingSlash: undefined}, '/'), + ).resolves.toBe('302 /%E6%96%87%E6%A1%A3/ → 200 index.html'); + }); + + it('keeps the baseUrl in redirects', async () => { + await expect( + serveRequest( + {baseUrl: '/café/', trailingSlash: true}, + '/caf%C3%A9/file', + ), + ).resolves.toBe('301 /caf%C3%A9/file/ → 200 file.html'); + }); + + it('serves a baseUrl percent-encoded in the config', async () => { + await expect( + serveRequest( + {baseUrl: '/caf%C3%A9/', trailingSlash: undefined}, + '/caf%C3%A9/', + ), + ).resolves.toBe('200 index.html'); }); }); }); diff --git a/packages/docusaurus/src/commands/serve/requestHandler.ts b/packages/docusaurus/src/commands/serve/requestHandler.ts index 6acb149a7e10..c8cb6fabd3b1 100644 --- a/packages/docusaurus/src/commands/serve/requestHandler.ts +++ b/packages/docusaurus/src/commands/serve/requestHandler.ts @@ -21,12 +21,16 @@ type ServeRequestHandler = ( export function createServeRequestHandler({ outDir, - baseUrl, + baseUrl: configBaseUrl, trailingSlash, }: {outDir: string} & Pick< DocusaurusConfig, 'baseUrl' | 'trailingSlash' >): ServeRequestHandler { + // Browsers send the baseUrl percent-encoded, like "/caf%C3%A9/" + // A baseUrl already percent-encoded in the config stays the same + const baseUrl = parseURLPath(configBaseUrl).pathname; + return async (req, res) => { // Browsers must revalidate every response, so that a rebuild or a config // change never leaves a stale page or redirect in their cache