From d60c74243f639761ad735d66d814e627f8f847a2 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 4 Feb 2025 09:48:41 +0000 Subject: [PATCH] fix: ignore trailing slash for endpoints with file extensions (#13131) * fix: ignore trailing slash for endpoints with file extensions * Fix test --- .changeset/purple-suits-matter.md | 5 ++++ .../astro/src/core/routing/manifest/create.ts | 24 ++++++++++++------- .../test/units/routing/trailing-slash.test.js | 17 +++++++++---- 3 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 .changeset/purple-suits-matter.md diff --git a/.changeset/purple-suits-matter.md b/.changeset/purple-suits-matter.md new file mode 100644 index 0000000000..998a5fcaa5 --- /dev/null +++ b/.changeset/purple-suits-matter.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Ignores trailing slashes for endpoints with file extensions in the route diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 19527851c0..692d47bebe 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -19,7 +19,7 @@ import { UnsupportedExternalRedirect, } from '../../errors/errors-data.js'; import { AstroError } from '../../errors/index.js'; -import { removeLeadingForwardSlash, slash } from '../../path.js'; +import { hasFileExtension, removeLeadingForwardSlash, slash } from '../../path.js'; import { injectServerIslandRoute } from '../../server-islands/endpoint.js'; import { resolvePages } from '../../util.js'; import { ensure404Route } from '../astro-designed-error-pages.js'; @@ -218,12 +218,12 @@ function createFileBasedRoutes( } else { components.push(item.file); const component = item.file; - const { trailingSlash } = settings.config; - const pattern = getPattern(segments, settings.config.base, trailingSlash); - const generate = getRouteGenerator(segments, trailingSlash); const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; + const trailingSlash = trailingSlashForPath(pathname, settings.config); + const pattern = getPattern(segments, settings.config.base, trailingSlash); + const generate = getRouteGenerator(segments, trailingSlash); const route = joinSegments(segments); routes.push({ route, @@ -257,6 +257,14 @@ function createFileBasedRoutes( return routes; } +// Get trailing slash rule for a path, based on the config and whether the path has an extension. +// TODO: in Astro 6, change endpoints with extentions to use 'never' +const trailingSlashForPath = ( + pathname: string | null, + config: AstroConfig, +): AstroConfig['trailingSlash'] => + pathname && hasFileExtension(pathname) ? 'ignore' : config.trailingSlash; + function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): RouteData[] { const { config } = settings; const prerender = getPrerenderDefault(config); @@ -276,13 +284,13 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Rou }); const type = resolved.endsWith('.astro') ? 'page' : 'endpoint'; - const { trailingSlash } = config; - - const pattern = getPattern(segments, settings.config.base, trailingSlash); - const generate = getRouteGenerator(segments, trailingSlash); const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; + + const trailingSlash = trailingSlashForPath(pathname, config); + const pattern = getPattern(segments, settings.config.base, trailingSlash); + const generate = getRouteGenerator(segments, trailingSlash); const params = segments .flat() .filter((p) => p.dynamic) diff --git a/packages/astro/test/units/routing/trailing-slash.test.js b/packages/astro/test/units/routing/trailing-slash.test.js index d3d349c9ea..77102c15e3 100644 --- a/packages/astro/test/units/routing/trailing-slash.test.js +++ b/packages/astro/test/units/routing/trailing-slash.test.js @@ -95,6 +95,16 @@ describe('trailingSlash', () => { assert.equal(res.statusCode, 404); }); + it('should match an injected route when request has a file extension and no slash', async () => { + const { req, res, text } = createRequestAndResponse({ + method: 'GET', + url: '/injected.json', + }); + container.handle(req, res); + const json = await text(); + assert.equal(json, '{"success":true}'); + }); + it('should match the API route when request has a trailing slash, with a file extension', async () => { const { req, res, text } = createRequestAndResponse({ method: 'GET', @@ -105,14 +115,13 @@ describe('trailingSlash', () => { assert.equal(json, '{"success":true}'); }); - it('should NOT match the API route when request lacks a trailing slash, with a file extension', async () => { + it('should also match the API route when request lacks a trailing slash, with a file extension', async () => { const { req, res, text } = createRequestAndResponse({ method: 'GET', url: '/dot.json', }); container.handle(req, res); - const html = await text(); - assert.equal(html.includes(`Not found`), true); - assert.equal(res.statusCode, 404); + const json = await text(); + assert.equal(json, '{"success":true}'); }); });