Skip to content

Commit b71bd10

Browse files
authored
fix: correct handling of collapsing slashes (#13130)
1 parent c497491 commit b71bd10

File tree

7 files changed

+57
-9
lines changed

7 files changed

+57
-9
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/internal-helpers': patch
3+
---
4+
5+
Fixes a bug that meant that internal as well as trailing duplicate slashes were collapsed

.changeset/pink-apes-invite.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+
Fixes a bug that caused duplicate slashes inside query params to be collapsed

packages/astro/src/vite-plugin-astro-server/trailing-slash.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,23 @@ export function trailingSlashMiddleware(settings: AstroSettings): vite.Connect.N
99
const { trailingSlash } = settings.config;
1010

1111
return function devTrailingSlash(req, res, next) {
12-
const url = req.url!;
13-
14-
const destination = collapseDuplicateTrailingSlashes(url, true);
15-
if (url && destination !== url) {
16-
return writeRedirectResponse(res, 301, destination);
17-
}
12+
const url = new URL(`http://localhost${req.url}`);
1813
let pathname: string;
1914
try {
20-
pathname = decodeURI(new URL(url, 'http://localhost').pathname);
15+
pathname = decodeURI(url.pathname);
2116
} catch (e) {
2217
/* malformed uri */
2318
return next(e);
2419
}
2520
if (pathname.startsWith('/_') || pathname.startsWith('/@')) {
2621
return next();
2722
}
23+
24+
const destination = collapseDuplicateTrailingSlashes(pathname, true);
25+
if (pathname && destination !== pathname) {
26+
return writeRedirectResponse(res, 301, `${destination}${url.search}`);
27+
}
28+
2829
if (
2930
(trailingSlash === 'never' && pathname.endsWith('/') && pathname !== '/') ||
3031
(trailingSlash === 'always' && !pathname.endsWith('/') && !hasFileExtension(pathname))

packages/astro/test/dev-routing.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ describe('Development Routing', () => {
6060
assert.equal(response.headers.get('Location'), '/');
6161
});
6262

63+
it('does not redirect multiple internal slashes', async () => {
64+
const response = await fixture.fetch('/another///here', { redirect: 'manual' });
65+
assert.equal(response.status, 404);
66+
});
67+
68+
it('does not redirect slashes on query params', async () => {
69+
const response = await fixture.fetch('/another?foo=bar///', { redirect: 'manual' });
70+
assert.equal(response.status, 200);
71+
});
72+
73+
it('does redirect multiple trailing slashes with query params', async () => {
74+
const response = await fixture.fetch('/another///?foo=bar///', { redirect: 'manual' });
75+
assert.equal(response.status, 301);
76+
assert.equal(response.headers.get('Location'), '/another/?foo=bar///');
77+
});
78+
6379
it('404 when loading invalid dynamic route', async () => {
6480
const response = await fixture.fetch('/2');
6581
assert.equal(response.status, 404);

packages/astro/test/ssr-trailing-slash.js renamed to packages/astro/test/ssr-trailing-slash.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,28 @@ describe('Redirecting trailing slashes in SSR', () => {
3333
assert.equal(response.headers.get('Location'), '/another/');
3434
});
3535

36+
it('Redirects to collapse multiple trailing slashes with query param', async () => {
37+
const app = await fixture.loadTestAdapterApp();
38+
const request = new Request('http://example.com/another///?hello=world');
39+
const response = await app.render(request);
40+
assert.equal(response.status, 301);
41+
assert.equal(response.headers.get('Location'), '/another/?hello=world');
42+
});
43+
44+
it('Does not redirect to collapse multiple internal slashes', async () => {
45+
const app = await fixture.loadTestAdapterApp();
46+
const request = new Request('http://example.com/another///path/');
47+
const response = await app.render(request);
48+
assert.equal(response.status, 404);
49+
});
50+
51+
it('Does not redirect trailing slashes on query params', async () => {
52+
const app = await fixture.loadTestAdapterApp();
53+
const request = new Request('http://example.com/another/?hello=world///');
54+
const response = await app.render(request);
55+
assert.equal(response.status, 200);
56+
});
57+
3658
it('Does not redirect when trailing slash is present', async () => {
3759
const app = await fixture.loadTestAdapterApp();
3860
const request = new Request('http://example.com/another/');

packages/internal-helpers/src/path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function collapseDuplicateSlashes(path: string) {
1919
return path.replace(/(?<!:)\/{2,}/g, '/');
2020
}
2121

22-
export const MANY_TRAILING_SLASHES = /\/{2,}/g;
22+
export const MANY_TRAILING_SLASHES = /\/{2,}$/g;
2323

2424
export function collapseDuplicateTrailingSlashes(path: string, trailingSlash: boolean) {
2525
if (!path) {

pnpm-lock.yaml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)