From 6f92832bd35e294d2c77c3efbdbc95afa0c64a1e Mon Sep 17 00:00:00 2001 From: astrobot-houston Date: Tue, 30 Jun 2026 18:14:13 +0000 Subject: [PATCH] fix: prevent trailing slash on dynamic file endpoints during build (#17241)\n\nOverride trailingSlash to 'never' in stringifyParams for endpoint routes\nwith file extensions, matching the route pattern generated by\ntrailingSlashForPath in create-manifest.ts. This fixes a regression from\n#17224 where dynamic file endpoints (e.g. [name].json.ts) with\ntrailingSlash: 'always' would fail to build with TypeError: Missing\nparameter." --- .changeset/dark-melons-boil.md | 5 +++ packages/astro/src/core/routing/params.ts | 9 +++- .../test/units/routing/generator.test.ts | 43 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/dark-melons-boil.md diff --git a/.changeset/dark-melons-boil.md b/.changeset/dark-melons-boil.md new file mode 100644 index 000000000000..2a6f032c46cc --- /dev/null +++ b/.changeset/dark-melons-boil.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a build failure for dynamic file endpoints (e.g. `[name].json.ts`) when `trailingSlash` is set to `"always"` diff --git a/packages/astro/src/core/routing/params.ts b/packages/astro/src/core/routing/params.ts index 0f9bae073cfb..1595b6cba1fc 100644 --- a/packages/astro/src/core/routing/params.ts +++ b/packages/astro/src/core/routing/params.ts @@ -1,3 +1,4 @@ +import { hasFileExtension } from '@astrojs/internal-helpers/path'; import type { GetStaticPathsItem } from '../../types/public/common.js'; import type { AstroConfig } from '../../types/public/index.js'; import type { RouteData } from '../../types/public/internal.js'; @@ -15,6 +16,12 @@ export function stringifyParams( route: RouteData, trailingSlash: AstroConfig['trailingSlash'], ) { + // File endpoints (e.g. [name].json.ts) should never have a trailing slash, + // even when trailingSlash is set to 'always'. This matches the route pattern + // generated by trailingSlashForPath in create-manifest.ts. + const effectiveTrailingSlash = + route.type === 'endpoint' && hasFileExtension(route.route) ? 'never' : trailingSlash; + // validate parameter values then stringify each value const validatedParams: Record = {}; for (const [key, value] of Object.entries(params)) { @@ -24,5 +31,5 @@ export function stringifyParams( } } - return getRouteGenerator(route.segments, trailingSlash)(validatedParams); + return getRouteGenerator(route.segments, effectiveTrailingSlash)(validatedParams); } diff --git a/packages/astro/test/units/routing/generator.test.ts b/packages/astro/test/units/routing/generator.test.ts index 32d0fe72a16d..3e3c7c929c4b 100644 --- a/packages/astro/test/units/routing/generator.test.ts +++ b/packages/astro/test/units/routing/generator.test.ts @@ -3,6 +3,8 @@ import { describe, it } from 'node:test'; import type { AstroConfig } from '../../../dist/types/public/config.js'; import type { RoutePart } from '../../../dist/types/public/internal.js'; import { getRouteGenerator } from '../../../dist/core/routing/generator.js'; +import { stringifyParams } from '../../../dist/core/routing/params.js'; +import { dynamicPart, makeRoute, staticPart } from './test-helpers.ts'; interface TestCase { routeData: RoutePart[][]; @@ -160,3 +162,44 @@ describe('routing - generator', () => { assert.throws(() => generator({}), TypeError); }); }); + +describe('stringifyParams - file endpoint trailing slash', () => { + it('does not append trailing slash to dynamic file endpoints with trailingSlash always (issue #17241)', () => { + const route = makeRoute({ + segments: [[staticPart('api')], [dynamicPart('name'), staticPart('.json')]], + trailingSlash: 'never', + route: '/api/[name].json', + pathname: undefined, + type: 'endpoint', + }); + + const result = stringifyParams({ name: 'foo' }, route, 'always'); + assert.equal(result, '/api/foo.json', 'should not have trailing slash'); + }); + + it('still appends trailing slash to dynamic page routes with trailingSlash always', () => { + const route = makeRoute({ + segments: [[staticPart('blog')], [dynamicPart('slug')]], + trailingSlash: 'always', + route: '/blog/[slug]', + pathname: undefined, + type: 'page', + }); + + const result = stringifyParams({ slug: 'hello' }, route, 'always'); + assert.equal(result, '/blog/hello/', 'should have trailing slash'); + }); + + it('does not append trailing slash to dynamic endpoints without file extensions with trailingSlash never', () => { + const route = makeRoute({ + segments: [[staticPart('api')], [dynamicPart('name')]], + trailingSlash: 'never', + route: '/api/[name]', + pathname: undefined, + type: 'endpoint', + }); + + const result = stringifyParams({ name: 'foo' }, route, 'never'); + assert.equal(result, '/api/foo', 'should not have trailing slash'); + }); +});