diff --git a/.changeset/stale-geese-open.md b/.changeset/stale-geese-open.md new file mode 100644 index 000000000..b8b683b2a --- /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/src/utils.js b/src/utils.js index e09b8102b..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,9 +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 - else if (typeof req.originalUrl !== "undefined") { - return req.originalUrl; + // Fastify decodes URI by default, our logic is based on encoded URI. + // `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 === decodeURI(req.originalUrl)) { + return req.originalUrl; + } + } catch { + // decodeURI can throw on malformed sequences, fall through + } } return req.url; diff --git a/test/middleware.test.js b/test/middleware.test.js index 634db451d..8daed5c93 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -3473,6 +3473,77 @@ 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", + ); + expect(response.text).toBe("HTML with spaces"); + }); + }); + } + describe("should work and don't call the next middleware for finished or errored requests by default", () => { let compiler; 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