From c432be97c10099e4036ddec6d4bcdd9e6e83eb02 Mon Sep 17 00:00:00 2001 From: caohuilin Date: Thu, 18 Dec 2025 21:17:54 +0800 Subject: [PATCH] fix: escape special characters in static file routes to prevent regex syntax errors --- .../server/core/src/plugins/render/index.ts | 26 ++++++++++++++++--- .../server-prod/config/public/test(bug.txt | 1 + .../server-prod/tests/index.test.ts | 8 ++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/integration/server-prod/config/public/test(bug.txt diff --git a/packages/server/core/src/plugins/render/index.ts b/packages/server/core/src/plugins/render/index.ts index 22aa13c82aff..bf029687f0a4 100644 --- a/packages/server/core/src/plugins/render/index.ts +++ b/packages/server/core/src/plugins/render/index.ts @@ -14,6 +14,18 @@ import { requestLatencyMiddleware } from '../monitors'; export * from './inject'; +const DYNAMIC_ROUTE_REG = /\/:./; + +/** + * Escape special regex characters in a path string. + * This is needed because Hono's router converts paths to regex patterns, + * and special characters like parentheses need to be escaped. + */ +function escapeRegexSpecialChars(path: string): string { + // Escape special regex characters: ( ) [ ] { } * + ? . ^ $ | \ + return path.replace(/[()[\]{}*+?.^$|\\]/g, '\\$&'); +} + export const renderPlugin = (): ServerPlugin => ({ name: '@modern-js/plugin-render', @@ -35,9 +47,17 @@ export const renderPlugin = (): ServerPlugin => ({ for (const route of pageRoutes) { const { urlPath: originUrlPath } = route; - const urlPath = originUrlPath.endsWith('/') - ? `${originUrlPath}*` - : `${originUrlPath}/*`; + const isDynamic = DYNAMIC_ROUTE_REG.test(originUrlPath); + + // For static routes, escape special regex characters to prevent regex syntax errors + // For dynamic routes, keep as-is since they contain route parameters + const escapedPath = isDynamic + ? originUrlPath + : escapeRegexSpecialChars(originUrlPath); + + const urlPath = escapedPath.endsWith('/') + ? `${escapedPath}*` + : `${escapedPath}/*`; // config.renderMiddlewares can register by server config and prepare hook renderMiddlewares?.forEach(m => { diff --git a/tests/integration/server-prod/config/public/test(bug.txt b/tests/integration/server-prod/config/public/test(bug.txt new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/tests/integration/server-prod/config/public/test(bug.txt @@ -0,0 +1 @@ +test diff --git a/tests/integration/server-prod/tests/index.test.ts b/tests/integration/server-prod/tests/index.test.ts index 8454fcff9bc7..aef2cf9335fa 100644 --- a/tests/integration/server-prod/tests/index.test.ts +++ b/tests/integration/server-prod/tests/index.test.ts @@ -89,4 +89,12 @@ describe('test basic usage', () => { expect(status).toBe(successStatus); expect(headers['content-type']).toBe('image/png'); }); + + test(`should serve static file with special characters in filename`, async () => { + const { status, data } = await axios.get( + `http://localhost:${appPort}/test(bug.txt`, + ); + expect(status).toBe(successStatus); + expect(data.trim()).toBe('test'); + }); });