0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-10 23:01:26 -05:00

fix: correct handling of collapsing slashes (#13130)

This commit is contained in:
Matt Kane 2025-02-04 09:44:01 +00:00 committed by GitHub
parent c497491cfe
commit b71bd10989
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 57 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/internal-helpers': patch
---
Fixes a bug that meant that internal as well as trailing duplicate slashes were collapsed

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a bug that caused duplicate slashes inside query params to be collapsed

View file

@ -9,15 +9,10 @@ export function trailingSlashMiddleware(settings: AstroSettings): vite.Connect.N
const { trailingSlash } = settings.config; const { trailingSlash } = settings.config;
return function devTrailingSlash(req, res, next) { return function devTrailingSlash(req, res, next) {
const url = req.url!; const url = new URL(`http://localhost${req.url}`);
const destination = collapseDuplicateTrailingSlashes(url, true);
if (url && destination !== url) {
return writeRedirectResponse(res, 301, destination);
}
let pathname: string; let pathname: string;
try { try {
pathname = decodeURI(new URL(url, 'http://localhost').pathname); pathname = decodeURI(url.pathname);
} catch (e) { } catch (e) {
/* malformed uri */ /* malformed uri */
return next(e); return next(e);
@ -25,6 +20,12 @@ export function trailingSlashMiddleware(settings: AstroSettings): vite.Connect.N
if (pathname.startsWith('/_') || pathname.startsWith('/@')) { if (pathname.startsWith('/_') || pathname.startsWith('/@')) {
return next(); return next();
} }
const destination = collapseDuplicateTrailingSlashes(pathname, true);
if (pathname && destination !== pathname) {
return writeRedirectResponse(res, 301, `${destination}${url.search}`);
}
if ( if (
(trailingSlash === 'never' && pathname.endsWith('/') && pathname !== '/') || (trailingSlash === 'never' && pathname.endsWith('/') && pathname !== '/') ||
(trailingSlash === 'always' && !pathname.endsWith('/') && !hasFileExtension(pathname)) (trailingSlash === 'always' && !pathname.endsWith('/') && !hasFileExtension(pathname))

View file

@ -60,6 +60,22 @@ describe('Development Routing', () => {
assert.equal(response.headers.get('Location'), '/'); assert.equal(response.headers.get('Location'), '/');
}); });
it('does not redirect multiple internal slashes', async () => {
const response = await fixture.fetch('/another///here', { redirect: 'manual' });
assert.equal(response.status, 404);
});
it('does not redirect slashes on query params', async () => {
const response = await fixture.fetch('/another?foo=bar///', { redirect: 'manual' });
assert.equal(response.status, 200);
});
it('does redirect multiple trailing slashes with query params', async () => {
const response = await fixture.fetch('/another///?foo=bar///', { redirect: 'manual' });
assert.equal(response.status, 301);
assert.equal(response.headers.get('Location'), '/another/?foo=bar///');
});
it('404 when loading invalid dynamic route', async () => { it('404 when loading invalid dynamic route', async () => {
const response = await fixture.fetch('/2'); const response = await fixture.fetch('/2');
assert.equal(response.status, 404); assert.equal(response.status, 404);

View file

@ -33,6 +33,28 @@ describe('Redirecting trailing slashes in SSR', () => {
assert.equal(response.headers.get('Location'), '/another/'); assert.equal(response.headers.get('Location'), '/another/');
}); });
it('Redirects to collapse multiple trailing slashes with query param', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/another///?hello=world');
const response = await app.render(request);
assert.equal(response.status, 301);
assert.equal(response.headers.get('Location'), '/another/?hello=world');
});
it('Does not redirect to collapse multiple internal slashes', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/another///path/');
const response = await app.render(request);
assert.equal(response.status, 404);
});
it('Does not redirect trailing slashes on query params', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/another/?hello=world///');
const response = await app.render(request);
assert.equal(response.status, 200);
});
it('Does not redirect when trailing slash is present', async () => { it('Does not redirect when trailing slash is present', async () => {
const app = await fixture.loadTestAdapterApp(); const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/another/'); const request = new Request('http://example.com/another/');

View file

@ -19,7 +19,7 @@ export function collapseDuplicateSlashes(path: string) {
return path.replace(/(?<!:)\/{2,}/g, '/'); return path.replace(/(?<!:)\/{2,}/g, '/');
} }
export const MANY_TRAILING_SLASHES = /\/{2,}/g; export const MANY_TRAILING_SLASHES = /\/{2,}$/g;
export function collapseDuplicateTrailingSlashes(path: string, trailingSlash: boolean) { export function collapseDuplicateTrailingSlashes(path: string, trailingSlash: boolean) {
if (!path) { if (!path) {

1
pnpm-lock.yaml generated
View file

@ -8938,7 +8938,6 @@ packages:
libsql@0.4.5: libsql@0.4.5:
resolution: {integrity: sha512-sorTJV6PNt94Wap27Sai5gtVLIea4Otb2LUiAUyr3p6BPOScGMKGt5F1b5X/XgkNtcsDKeX5qfeBDj+PdShclQ==} resolution: {integrity: sha512-sorTJV6PNt94Wap27Sai5gtVLIea4Otb2LUiAUyr3p6BPOScGMKGt5F1b5X/XgkNtcsDKeX5qfeBDj+PdShclQ==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32] os: [darwin, linux, win32]
lightningcss-darwin-arm64@1.29.1: lightningcss-darwin-arm64@1.29.1: