From 58ed112013fcfaf0fa84d9ffac73bc0df89b8184 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 5 Apr 2026 14:07:12 -0500 Subject: [PATCH 1/3] fix: improve URL handling in getRequestURL function --- src/utils.js | 24 ++++++++++++-- test/middleware.test.js | 70 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/utils.js b/src/utils.js index e09b8102b..e95e90d8a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -312,9 +312,27 @@ function getRequestURL(req) { if (typeof req.getURL === "function") { return req.getURL(); } - // Fastify decodes URI by default, Our logic is based on encoded URI - else if (typeof req.originalUrl !== "undefined") { - return req.originalUrl; + + // Fastify decodes URI by default, our logic is based on encoded URI. + // req.originalUrl preserves the original encoded URL; req.url may be + // modified by middleware (e.g. connect-history-api-fallback), in which + // case we use req.url instead. + if (typeof req.originalUrl !== "undefined") { + // If req.url is just the decoded form of req.originalUrl (Fastify behavior), + // return the original encoded URL. Otherwise middleware modified req.url. + try { + if ( + req.url === req.originalUrl || + (typeof req.url !== "undefined" && + decodeURI(req.originalUrl) === req.url) + ) { + return req.originalUrl; + } + } catch { + // decodeURI can throw on malformed sequences, fall through + } + + return req.url; } return req.url; diff --git a/test/middleware.test.js b/test/middleware.test.js index 634db451d..9d1499233 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -3473,6 +3473,76 @@ describe.each([ }); }); + if (!["hapi", "hono"].includes(name)) { + describe("should work when req.url is modified by middleware to a file with encoded characters", () => { + let compiler; + + const outputPath = path.resolve( + __dirname, + "./outputs/basic-test-modified-url-encoded", + ); + + beforeAll(async () => { + compiler = getCompiler({ + ...webpackConfig, + output: { + filename: "bundle.js", + path: outputPath, + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + {}, + { + setupMiddlewares: (middlewares) => { + if (name === "koa") { + middlewares.unshift(async (ctx, next) => { + ctx.url = "/file with spaces.html"; + + await next(); + }); + } else { + middlewares.unshift({ + route: "/", + fn: (oldReq, res, next) => { + oldReq.url = "/file with spaces.html"; + next(); + }, + }); + } + + return middlewares; + }, + }, + ); + + instance.context.outputFileSystem.mkdirSync(outputPath, { + recursive: true, + }); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "file with spaces.html"), + "HTML with spaces", + ); + }); + + afterAll(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request when req.url is rewritten to a path with spaces', async () => { + const response = await req.get("/any-path"); + + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toBe( + "text/html; charset=utf-8", + ); + }); + }); + } + describe("should work and don't call the next middleware for finished or errored requests by default", () => { let compiler; From 7476ce4ad9134752dba09cc9329fa6a70e4cb11d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 5 Apr 2026 14:17:48 -0500 Subject: [PATCH 2/3] docs: add changelog --- .changeset/stale-geese-open.md | 5 +++++ test/middleware.test.js | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/stale-geese-open.md diff --git a/.changeset/stale-geese-open.md b/.changeset/stale-geese-open.md new file mode 100644 index 000000000..84704a83c --- /dev/null +++ b/.changeset/stale-geese-open.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-middleware": patch +--- + +respect `req.url` when modified by middleware such as `connect-history-api-fallback` diff --git a/test/middleware.test.js b/test/middleware.test.js index 9d1499233..8daed5c93 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -3539,6 +3539,7 @@ describe.each([ expect(response.headers["content-type"]).toBe( "text/html; charset=utf-8", ); + expect(response.text).toBe("HTML with spaces"); }); }); } From 911c2921f4eeb0a6ac7d181e40aa9e8c527abee2 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 7 Apr 2026 18:04:51 +0300 Subject: [PATCH 3/3] refactor: logic --- .changeset/stale-geese-open.md | 2 +- src/utils.js | 19 +++++-------------- types/utils.d.ts | 5 +++++ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.changeset/stale-geese-open.md b/.changeset/stale-geese-open.md index 84704a83c..b8b683b2a 100644 --- a/.changeset/stale-geese-open.md +++ b/.changeset/stale-geese-open.md @@ -2,4 +2,4 @@ "webpack-dev-middleware": patch --- -respect `req.url` when modified by middleware such as `connect-history-api-fallback` +Respect `req.url` when modified by middleware such as `connect-history-api-fallback`. diff --git a/src/utils.js b/src/utils.js index e95e90d8a..a3e8bb88e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -255,6 +255,7 @@ function parseTokenList(str) { * @property {(() => string | undefined)=} getMethod get method extra method * @property {(() => string | undefined)=} getURL get URL extra method * @property {string=} originalUrl an extra option for `fastify` (and `@fastify/express`) to get original URL + * @property {string=} id an extra option for `fastify` (and `@fastify/express`) to get ID of request */ /** @@ -312,27 +313,17 @@ function getRequestURL(req) { if (typeof req.getURL === "function") { return req.getURL(); } - // Fastify decodes URI by default, our logic is based on encoded URI. - // req.originalUrl preserves the original encoded URL; req.url may be - // modified by middleware (e.g. connect-history-api-fallback), in which - // case we use req.url instead. - if (typeof req.originalUrl !== "undefined") { - // If req.url is just the decoded form of req.originalUrl (Fastify behavior), - // return the original encoded URL. Otherwise middleware modified req.url. + // `req.url` may be modified by middleware (e.g. connect-history-api-fallback), in which case we use req.url instead. + // `req.id` is a special property of `fastify` + else if (req.id && req.originalUrl) { try { - if ( - req.url === req.originalUrl || - (typeof req.url !== "undefined" && - decodeURI(req.originalUrl) === req.url) - ) { + if (req.url === decodeURI(req.originalUrl)) { return req.originalUrl; } } catch { // decodeURI can throw on malformed sequences, fall through } - - return req.url; } return req.url; diff --git a/types/utils.d.ts b/types/utils.d.ts index 9041371b1..5c159413c 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -18,6 +18,10 @@ export type ExpectedIncomingMessage = { * an extra option for `fastify` (and `@fastify/express`) to get original URL */ originalUrl?: string | undefined; + /** + * an extra option for `fastify` (and `@fastify/express`) to get ID of request + */ + id?: string | undefined; }; export type ExpectedServerResponse = { /** @@ -147,6 +151,7 @@ export function getOutgoing< * @property {(() => string | undefined)=} getMethod get method extra method * @property {(() => string | undefined)=} getURL get URL extra method * @property {string=} originalUrl an extra option for `fastify` (and `@fastify/express`) to get original URL + * @property {string=} id an extra option for `fastify` (and `@fastify/express`) to get ID of request */ /** * @typedef {object} ExpectedServerResponse