From fe678c407d70b37f9c64fd1be31f762c21a52400 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 27 Jan 2026 20:02:05 +0000 Subject: [PATCH 1/6] fix: update redirect logic for URLs ending with '/' --- src/utils/getFilenameFromUrl.js | 11 ++++++++++- test/middleware.test.js | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index d54bd749a..efad5b0f5 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -50,7 +50,6 @@ class FilenameError extends Error { } } -// TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586 /** * @template {IncomingMessage} Request * @template {ServerResponse} Response @@ -120,6 +119,16 @@ function getFilenameFromUrl(context, url) { ); try { + if (filename[filename.length - 1] === "/") { + if (options.index === false) { + return; + } else if (options.index === "string") { + filename = path.join(filename, options.index); + } else { + filename = path.join(filename, "index.html"); + } + } + extra.stats = context.outputFileSystem.statSync(filename); } catch { continue; diff --git a/test/middleware.test.js b/test/middleware.test.js index 3c0836a8a..02969c2ee 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -1736,6 +1736,10 @@ describe.each([ value: "noextension", code: 200, }, + { + value: "noextension/", + code: 404, + }, ], }, { @@ -1780,6 +1784,11 @@ describe.each([ contentType: "text/plain; charset=utf-8", code: 200, }, + { + value: "windows%202.txt/", + contentType: get404ContentTypeHeader(name), + code: 404, + }, ], }, { @@ -1945,7 +1954,7 @@ describe.each([ expect(response.statusCode).toEqual(code); - if (data) { + if (data && code !== 404) { expect(response.headers["content-length"]).toEqual( String(data.length), ); @@ -5187,6 +5196,15 @@ describe.each([ "text/html; charset=utf-8", ); }); + + it('should return the "404" code for the "GET" request with a non-existent file', async () => { + const response = await req.get("/default.html/"); + + expect(response.statusCode).toBe(404); + expect(response.headers["content-type"]).toBe( + get404ContentTypeHeader(name), + ); + }); }); describe('should work with "string" value with a custom extension', () => { From 6a8465e66e887ce4427227cfbc81ead153226239 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 27 Jan 2026 20:42:48 +0000 Subject: [PATCH 2/6] test: add 404 response check for GET request to "index.html" --- test/middleware.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/middleware.test.js b/test/middleware.test.js index 02969c2ee..cbc48b703 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -5109,6 +5109,15 @@ describe.each([ ); }); + it('should return the "404" code for the "GET" request to the "index.html" file', async () => { + const response = await req.get("/index.html/"); + + expect(response.statusCode).toBe(404); + expect(response.headers["content-type"]).toEqual( + get404ContentTypeHeader(name), + ); + }); + it('should return the "200" code for the "GET" request to the "index.html" file', async () => { const response = await req.get("/index.html"); From 4b0b8982a35e2e5ad6958095358537da29fa757a Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 28 Jan 2026 17:23:47 +0000 Subject: [PATCH 3/6] test: add 200 and 404 response checks for GET requests to "/slug/" and "/slug" paths --- src/utils/getFilenameFromUrl.js | 2 +- test/middleware.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index efad5b0f5..6cb4366db 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -122,7 +122,7 @@ function getFilenameFromUrl(context, url) { if (filename[filename.length - 1] === "/") { if (options.index === false) { return; - } else if (options.index === "string") { + } else if (typeof options.index === "string") { filename = path.join(filename, options.index); } else { filename = path.join(filename, "index.html"); diff --git a/test/middleware.test.js b/test/middleware.test.js index cbc48b703..ba7d38f44 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -5187,10 +5187,22 @@ describe.each([ instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); + + instance.context.outputFileSystem.mkdirSync( + path.resolve(outputPath, "slug"), + { + recursive: true, + }, + ); instance.context.outputFileSystem.writeFileSync( path.resolve(outputPath, "default.html"), "hello", ); + + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "slug", "default.html"), + "hello", + ); }); afterAll(async () => { @@ -5206,6 +5218,24 @@ describe.each([ ); }); + it('should return the "200" code for the "GET" request to the "/slug/" path', async () => { + const response = await req.get("/slug/"); + + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toBe( + "text/html; charset=utf-8", + ); + }); + + it('should return the "404" code for the "GET" request to the "/slug" path', async () => { + const response = await req.get("/slug"); + + expect(response.statusCode).toBe(404); + expect(response.headers["content-type"]).toEqual( + get404ContentTypeHeader(name), + ); + }); + it('should return the "404" code for the "GET" request with a non-existent file', async () => { const response = await req.get("/default.html/"); From 7f2a51b86e184db0759c3d5c82b3cd08f4c0a592 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 28 Jan 2026 17:29:47 +0000 Subject: [PATCH 4/6] test: update response code for GET request to "/slug" path from 404 to 200 --- test/middleware.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/middleware.test.js b/test/middleware.test.js index ba7d38f44..63cb8e90b 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -5227,12 +5227,12 @@ describe.each([ ); }); - it('should return the "404" code for the "GET" request to the "/slug" path', async () => { + it('should return the "200" code for the "GET" request to the "/slug" path', async () => { const response = await req.get("/slug"); - expect(response.statusCode).toBe(404); - expect(response.headers["content-type"]).toEqual( - get404ContentTypeHeader(name), + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toBe( + "text/html; charset=utf-8", ); }); From 8f5f508b212d080871de87c614e7a3dc55adf383 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 28 Jan 2026 17:38:26 +0000 Subject: [PATCH 5/6] fix: correct variable reference from filename to pathname in getFilenameFromUrl function --- src/utils/getFilenameFromUrl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index 6cb4366db..dc892cb8c 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -119,7 +119,7 @@ function getFilenameFromUrl(context, url) { ); try { - if (filename[filename.length - 1] === "/") { + if (pathname[pathname.length - 1] === "/") { if (options.index === false) { return; } else if (typeof options.index === "string") { From ee8725daac4326cc048078ce7695152e55bae181 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 28 Jan 2026 17:50:37 +0000 Subject: [PATCH 6/6] refactor: logic --- src/utils/getFilenameFromUrl.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index dc892cb8c..a71f9a94a 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -109,26 +109,29 @@ function getFilenameFromUrl(context, url) { throw new FilenameError("Forbidden", 403); } - // Strip the `pathname` property from the `publicPath` option from the start of requested url - // `/complex/foo.js` => `foo.js` - // and add outputPath - // `foo.js` => `/home/user/my-project/dist/foo.js` + let index; + + if (pathname && pathname.endsWith("/")) { + if (options.index === false) { + return; + } + index = + typeof options.index === "string" ? options.index : "index.html"; + } + + // Builds the absolute path of the file to serve: + // - If the URL ends with '/', appends the index file (index.html or custom) to the directory path. + // - If the URL does not end with '/', only joins the relative path to outputPath. + // Example: + // URL: /complex/foo.js => outputPath/complex/foo.js + // URL: /complex/ => outputPath/complex/index.html (or the configured index file) filename = path.join( outputPath, pathname.slice(publicPathPathname.length), + index || "", ); try { - if (pathname[pathname.length - 1] === "/") { - if (options.index === false) { - return; - } else if (typeof options.index === "string") { - filename = path.join(filename, options.index); - } else { - filename = path.join(filename, "index.html"); - } - } - extra.stats = context.outputFileSystem.statSync(filename); } catch { continue;