Skip to content

Commit 23425e2

Browse files
authored
Fix trailingSlash for extensionless endpoints in static builds (#16193)
1 parent 21f9fe2 commit 23425e2

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes `trailingSlash: "always"` producing redirect HTML instead of the actual response for extensionless endpoints during static builds

packages/astro/src/core/build/generate.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
prepareAssetsGenerationEnv,
1111
} from '../../assets/build/generate.js';
1212
import {
13+
appendForwardSlash,
1314
collapseDuplicateTrailingSlashes,
15+
hasFileExtension,
1416
joinPaths,
1517
removeLeadingForwardSlash,
1618
removeTrailingForwardSlash,
@@ -618,7 +620,13 @@ function getUrlForPath(
618620
}
619621
} else if (routeType === 'endpoint') {
620622
const buildPathRelative = removeLeadingForwardSlash(pathname);
621-
buildPathname = joinPaths(base, buildPathRelative);
623+
let endpointPathname = joinPaths(base, buildPathRelative);
624+
if (trailingSlash === 'always' && !hasFileExtension(pathname)) {
625+
endpointPathname = appendForwardSlash(endpointPathname);
626+
} else if (trailingSlash === 'never') {
627+
endpointPathname = removeTrailingForwardSlash(endpointPathname);
628+
}
629+
buildPathname = endpointPathname;
622630
} else {
623631
const buildPathRelative =
624632
removeTrailingForwardSlash(removeLeadingForwardSlash(pathname)) + ending;

packages/astro/test/units/build/generate.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,44 @@ describe('renderPath()', () => {
211211
assert.ok(errors.length > 0, 'error should be logged before re-throwing');
212212
});
213213

214+
// Regression: #16185 — extensionless endpoints with trailingSlash: 'always'
215+
// must have a trailing slash in the prerender request URL so that BaseApp.render()
216+
// does not emit a redirect instead of the endpoint's actual response.
217+
it('sends a trailing-slash request URL for extensionless endpoints when trailingSlash is always', async () => {
218+
const endpointOptions = await createStaticBuildOptions({
219+
inlineConfig: { trailingSlash: 'always' },
220+
});
221+
222+
let capturedUrl;
223+
const prerenderer = createMockPrerenderer({ '/demo': 'hello' });
224+
const originalRender = prerenderer.render.bind(prerenderer);
225+
prerenderer.render = async (request, opts) => {
226+
capturedUrl = new URL(request.url);
227+
return originalRender(request, opts);
228+
};
229+
230+
const route = createRouteData({
231+
route: '/demo',
232+
type: 'endpoint',
233+
trailingSlash: 'always',
234+
component: 'src/pages/demo.ts',
235+
});
236+
237+
await renderPath({
238+
prerenderer,
239+
pathname: '/demo',
240+
route,
241+
options: endpointOptions,
242+
logger: endpointOptions.logger,
243+
});
244+
245+
assert.ok(capturedUrl, 'prerenderer.render should have been called');
246+
assert.ok(
247+
capturedUrl.pathname.endsWith('/'),
248+
`expected trailing slash in request URL pathname, got "${capturedUrl.pathname}"`,
249+
);
250+
});
251+
214252
it('writes the rendered body to the filesystem (integration smoke)', async () => {
215253
const html = '<html><body>Written to disk</body></html>';
216254
const prerenderer = createMockPrerenderer({ '/disk-test': html });

0 commit comments

Comments
 (0)