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/afraid-kangaroos-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/internal-helpers': patch
---

Fixes a bug that meant that internal as well as trailing duplicate slashes were collapsed
5 changes: 5 additions & 0 deletions .changeset/pink-apes-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes a bug that caused duplicate slashes inside query params to be collapsed
15 changes: 8 additions & 7 deletions packages/astro/src/vite-plugin-astro-server/trailing-slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,23 @@ export function trailingSlashMiddleware(settings: AstroSettings): vite.Connect.N
const { trailingSlash } = settings.config;

return function devTrailingSlash(req, res, next) {
const url = req.url!;

const destination = collapseDuplicateTrailingSlashes(url, true);
if (url && destination !== url) {
return writeRedirectResponse(res, 301, destination);
}
const url = new URL(`http://localhost${req.url}`);
let pathname: string;
try {
pathname = decodeURI(new URL(url, 'http://localhost').pathname);
pathname = decodeURI(url.pathname);
} catch (e) {
/* malformed uri */
return next(e);
}
if (pathname.startsWith('/_') || pathname.startsWith('/@')) {
return next();
}

const destination = collapseDuplicateTrailingSlashes(pathname, true);
if (pathname && destination !== pathname) {
return writeRedirectResponse(res, 301, `${destination}${url.search}`);
}

if (
(trailingSlash === 'never' && pathname.endsWith('/') && pathname !== '/') ||
(trailingSlash === 'always' && !pathname.endsWith('/') && !hasFileExtension(pathname))
Expand Down
16 changes: 16 additions & 0 deletions packages/astro/test/dev-routing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ describe('Development Routing', () => {
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 () => {
const response = await fixture.fetch('/2');
assert.equal(response.status, 404);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ describe('Redirecting trailing slashes in SSR', () => {
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 () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/another/');
Expand Down
2 changes: 1 addition & 1 deletion packages/internal-helpers/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function collapseDuplicateSlashes(path: string) {
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) {
if (!path) {
Expand Down
1 change: 0 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.