Skip to content

Commit f5de51e

Browse files
authored
fix(routing): handle base path with trailing slash 'never' configuration (#13598)
* fix(routing): handle base path with trailing slash 'never' configuration * fix: changeset
1 parent 29ab38e commit f5de51e

3 files changed

Lines changed: 93 additions & 14 deletions

File tree

‎.changeset/puny-masks-laugh.md‎

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+
Fix routing with base paths when trailingSlash is set to 'never'. This ensures requests to '/base' are correctly matched when the base path is set to '/base', without requiring a trailing slash.

‎packages/astro/src/core/routing/rewrite.ts‎

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,45 @@ export function findRouteToRewrite({
5252
}
5353

5454
let pathname = newUrl.pathname;
55-
5655
const shouldAppendSlash = shouldAppendForwardSlash(trailingSlash, buildFormat);
56+
57+
// Special handling for base path
58+
if (base !== '/') {
59+
// Check if this is a request to the base path
60+
const isBasePathRequest = newUrl.pathname === base ||
61+
newUrl.pathname === removeTrailingForwardSlash(base);
5762

58-
if (base !== '/' && newUrl.pathname.startsWith(base)) {
59-
pathname = shouldAppendSlash
60-
? appendForwardSlash(newUrl.pathname)
61-
: removeTrailingForwardSlash(newUrl.pathname);
62-
pathname = pathname.slice(base.length);
63+
if (isBasePathRequest) {
64+
// For root path requests at the base URL
65+
// When trailingSlash is 'never', we should match '' (empty string pathname)
66+
// When trailingSlash is 'always', we should match '/' pathname
67+
pathname = shouldAppendSlash ? '/' : '';
68+
} else if (newUrl.pathname.startsWith(base)) {
69+
// For non-root paths under the base
70+
pathname = shouldAppendSlash
71+
? appendForwardSlash(newUrl.pathname)
72+
: removeTrailingForwardSlash(newUrl.pathname);
73+
pathname = pathname.slice(base.length);
74+
}
6375
}
6476

77+
// Ensure pathname starts with '/' when needed
6578
if (!pathname.startsWith('/') && shouldAppendSlash && newUrl.pathname.endsWith('/')) {
66-
// when base is in the rewrite call and trailingSlash is 'always' this is needed or it will 404.
6779
pathname = prependForwardSlash(pathname);
6880
}
6981

70-
if (pathname === '/' && base !== '/' && !shouldAppendSlash) {
71-
// when rewriting to index and trailingSlash is 'never' this is needed or it will 404
72-
// in this case the pattern will look for '/^$/' so '/' will never match
82+
// Convert '/' to '' for trailingSlash: 'never'
83+
if (pathname === '/' && !shouldAppendSlash) {
7384
pathname = '';
7485
}
7586

76-
newUrl.pathname = joinPaths(...[base, pathname].filter(Boolean));
87+
// Set the final URL pathname
88+
if (base !== '/' && (pathname === '' || pathname === '/') && !shouldAppendSlash) {
89+
// Special case for root path at base URL with trailingSlash: 'never'
90+
newUrl.pathname = removeTrailingForwardSlash(base);
91+
} else {
92+
newUrl.pathname = joinPaths(...[base, pathname].filter(Boolean));
93+
}
7794

7895
const decodedPathname = decodeURI(pathname);
7996
let foundRoute;

‎packages/astro/test/units/routing/trailing-slash.test.js‎

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ const fileSystem = {
1515
};
1616

1717
describe('trailingSlash', () => {
18+
let fixture;
1819
let container;
19-
let settings;
20+
let baseContainer;
2021

2122
before(async () => {
22-
const fixture = await createFixture(fileSystem);
23-
settings = await createBasicSettings({
23+
fixture = await createFixture(fileSystem);
24+
25+
// Create the first container with trailingSlash: 'always'
26+
const settings = await createBasicSettings({
2427
root: fixture.path,
2528
trailingSlash: 'always',
2629
output: 'server',
@@ -47,12 +50,44 @@ describe('trailingSlash', () => {
4750
settings,
4851
logger: defaultLogger,
4952
});
53+
54+
// Create the second container with base path and trailingSlash: 'never'
55+
const baseSettings = await createBasicSettings({
56+
root: fixture.path,
57+
trailingSlash: 'never',
58+
base: 'base',
59+
output: 'server',
60+
adapter: testAdapter(),
61+
integrations: [
62+
{
63+
name: 'test',
64+
hooks: {
65+
'astro:config:setup': ({ injectRoute }) => {
66+
injectRoute({
67+
pattern: '/',
68+
entrypoint: './src/pages/api.ts',
69+
});
70+
injectRoute({
71+
pattern: '/injected',
72+
entrypoint: './src/pages/api.ts',
73+
});
74+
},
75+
},
76+
},
77+
],
78+
});
79+
baseContainer = await createContainer({
80+
settings: baseSettings,
81+
logger: defaultLogger,
82+
});
5083
});
5184

5285
after(async () => {
5386
await container.close();
87+
await baseContainer.close();
5488
});
5589

90+
// Tests for trailingSlash: 'always'
5691
it('should match the API route when request has a trailing slash', async () => {
5792
const { req, res, text } = createRequestAndResponse({
5893
method: 'GET',
@@ -124,4 +159,26 @@ describe('trailingSlash', () => {
124159
const json = await text();
125160
assert.equal(json, '{"success":true}');
126161
});
162+
163+
// Tests for trailingSlash: 'never' with base path
164+
it('should not have trailing slash on root path when base is set and trailingSlash is never', async () => {
165+
const { req, res, text } = createRequestAndResponse({
166+
method: 'GET',
167+
url: '/base',
168+
});
169+
baseContainer.handle(req, res);
170+
const json = await text();
171+
assert.equal(json, '{"success":true}');
172+
});
173+
174+
it('should not match root path with trailing slash when base is set and trailingSlash is never', async () => {
175+
const { req, res, text } = createRequestAndResponse({
176+
method: 'GET',
177+
url: '/base/',
178+
});
179+
baseContainer.handle(req, res);
180+
const html = await text();
181+
assert.equal(html.includes(`<span class="statusMessage">Not found</span>`), true);
182+
assert.equal(res.statusCode, 404);
183+
});
127184
});

0 commit comments

Comments
 (0)