Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-endpoint-trailing-slash-static-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes `trailingSlash: "always"` producing redirect HTML instead of the actual response for extensionless endpoints during static builds
10 changes: 9 additions & 1 deletion packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
prepareAssetsGenerationEnv,
} from '../../assets/build/generate.js';
import {
appendForwardSlash,
collapseDuplicateTrailingSlashes,
hasFileExtension,
joinPaths,
removeLeadingForwardSlash,
removeTrailingForwardSlash,
Expand Down Expand Up @@ -618,7 +620,13 @@ function getUrlForPath(
}
} else if (routeType === 'endpoint') {
const buildPathRelative = removeLeadingForwardSlash(pathname);
buildPathname = joinPaths(base, buildPathRelative);
let endpointPathname = joinPaths(base, buildPathRelative);
if (trailingSlash === 'always' && !hasFileExtension(pathname)) {
endpointPathname = appendForwardSlash(endpointPathname);
} else if (trailingSlash === 'never') {
endpointPathname = removeTrailingForwardSlash(endpointPathname);
}
buildPathname = endpointPathname;
} else {
const buildPathRelative =
removeTrailingForwardSlash(removeLeadingForwardSlash(pathname)) + ending;
Expand Down
38 changes: 38 additions & 0 deletions packages/astro/test/units/build/generate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,44 @@ describe('renderPath()', () => {
assert.ok(errors.length > 0, 'error should be logged before re-throwing');
});

// Regression: #16185 — extensionless endpoints with trailingSlash: 'always'
// must have a trailing slash in the prerender request URL so that BaseApp.render()
// does not emit a redirect instead of the endpoint's actual response.
it('sends a trailing-slash request URL for extensionless endpoints when trailingSlash is always', async () => {
const endpointOptions = await createStaticBuildOptions({
inlineConfig: { trailingSlash: 'always' },
});

let capturedUrl;
const prerenderer = createMockPrerenderer({ '/demo': 'hello' });
const originalRender = prerenderer.render.bind(prerenderer);
prerenderer.render = async (request, opts) => {
capturedUrl = new URL(request.url);
return originalRender(request, opts);
};

const route = createRouteData({
route: '/demo',
type: 'endpoint',
trailingSlash: 'always',
component: 'src/pages/demo.ts',
});

await renderPath({
prerenderer,
pathname: '/demo',
route,
options: endpointOptions,
logger: endpointOptions.logger,
});

assert.ok(capturedUrl, 'prerenderer.render should have been called');
assert.ok(
capturedUrl.pathname.endsWith('/'),
`expected trailing slash in request URL pathname, got "${capturedUrl.pathname}"`,
);
});

it('writes the rendered body to the filesystem (integration smoke)', async () => {
const html = '<html><body>Written to disk</body></html>';
const prerenderer = createMockPrerenderer({ '/disk-test': html });
Expand Down
Loading