diff --git a/src/index.js b/src/index.js index 564f012..4ab8997 100644 --- a/src/index.js +++ b/src/index.js @@ -608,6 +608,12 @@ module.exports = async (request, response, config = {}, methods = {}) => { if (path.extname(relativePath) !== '') { try { stats = await handlers.lstat(absolutePath); + // If the path looks like it has an extension but actually resolves to a + // directory (e.g. /docs/1.4 where path.extname returns '.4'), clear stats + // so findRelated can fall back to index.html or the .html sibling. + if (stats && stats.isDirectory()) { + stats = null; + } } catch (err) { if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { return internalError(absolutePath, response, acceptsJSON, current, handlers, config, err); diff --git a/test/fixtures/1.2.html b/test/fixtures/1.2.html new file mode 100644 index 0000000..04e7aa6 --- /dev/null +++ b/test/fixtures/1.2.html @@ -0,0 +1 @@ +Version 1.2 diff --git a/test/fixtures/1.3.html b/test/fixtures/1.3.html new file mode 100644 index 0000000..8816432 --- /dev/null +++ b/test/fixtures/1.3.html @@ -0,0 +1 @@ +Version 1.3 html file diff --git a/test/fixtures/1.3/index.html b/test/fixtures/1.3/index.html new file mode 100644 index 0000000..ec9eff0 --- /dev/null +++ b/test/fixtures/1.3/index.html @@ -0,0 +1 @@ +Version 1.3 directory index diff --git a/test/fixtures/1.4/index.html b/test/fixtures/1.4/index.html new file mode 100644 index 0000000..e3def96 --- /dev/null +++ b/test/fixtures/1.4/index.html @@ -0,0 +1 @@ +Version 1.4 diff --git a/test/integration.test.js b/test/integration.test.js index cebc479..896a4ac 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1375,3 +1375,70 @@ test('etag header is set', async () => { '"ba114dbc69e41e180362234807f093c3c4628f90"' ); }); + +test('serve index.html for directory whose name contains a dot (e.g. /1.4)', async () => { + const target = '1.4'; + const index = path.join(fixturesFull, target, 'index.html'); + + const url = await getUrl({ + cleanUrls: true, + directoryListing: false, + trailingSlash: false + }); + + const response = await fetch(`${url}/${target}`); + const content = await fs.readFile(index, 'utf8'); + const text = await response.text(); + + expect(response.status).toBe(200); + expect(text).toBe(content); +}); + +test('redirect /1.4/ to /1.4 when trailingSlash is false', async () => { + const target = '1.4'; + + const url = await getUrl({ + cleanUrls: true, + directoryListing: false, + trailingSlash: false + }); + + const response = await fetch(`${url}/${target}/`, { + redirect: 'manual', + follow: 0 + }); + + const location = response.headers.get('location'); + expect(response.status).toBe(301); + expect(location).toBe(`${url}/${target}`); +}); + +test('serve 1.2.html via cleanUrls when requested as /1.2', async () => { + const url = await getUrl({ + cleanUrls: true, + directoryListing: false, + trailingSlash: false + }); + + const content = await fs.readFile(path.join(fixturesFull, '1.2.html'), 'utf8'); + const response = await fetch(`${url}/1.2`); + const text = await response.text(); + + expect(response.status).toBe(200); + expect(text).toBe(content); +}); + +test('prefer directory index.html over .html sibling when both 1.3/ and 1.3.html exist', async () => { + const url = await getUrl({ + cleanUrls: true, + directoryListing: false, + trailingSlash: false + }); + + const content = await fs.readFile(path.join(fixturesFull, '1.3', 'index.html'), 'utf8'); + const response = await fetch(`${url}/1.3`); + const text = await response.text(); + + expect(response.status).toBe(200); + expect(text).toBe(content); +});