diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index d54bd749a..a71f9a94a 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 @@ -110,13 +109,26 @@ 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 { diff --git a/test/middleware.test.js b/test/middleware.test.js index 3c0836a8a..63cb8e90b 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), ); @@ -5100,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"); @@ -5169,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 () => { @@ -5187,6 +5217,33 @@ describe.each([ "text/html; charset=utf-8", ); }); + + 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 "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 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', () => {