diff --git a/.changeset/twenty-cherries-switch.md b/.changeset/twenty-cherries-switch.md new file mode 100644 index 0000000000000..c79c53284e213 --- /dev/null +++ b/.changeset/twenty-cherries-switch.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a bug that caused the dev server to return an error if requesting "//" diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 6dbcf18aee687..1b7d30f9cd3e0 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -427,6 +427,10 @@ function detectRouteCollision(a: RouteData, b: RouteData, _config: AstroConfig, // Fallbacks are always added below other routes exactly to avoid collisions. return; } + // The double-slash redirect will never collide with any other route. + if (a.route === '//' || b.route === '//') { + return; + } if ( a.route === b.route && @@ -502,6 +506,27 @@ export async function createRouteManifest( } const redirectRoutes = createRedirectRoutes(params, routeMap, logger); + if (dev) { + // Add a redirect for `//` to `/`. We only do this in dev because different platforms + // vary a lot in how or whether they handle this sort of redirect, so we leave it to the adapter + redirectRoutes.push({ + type: 'redirect', + isIndex: false, + route: '//', + pattern: /^\/+$/, + segments: [], + params: [], + component: '/', + generate: () => '//', + pathname: '//', + prerender: false, + redirect: '/', + redirectRoute: routeMap.get('/'), + fallbackRoutes: [], + distURL: [], + origin: 'project', + }); + } // we remove the file based routes that were deemed redirects const filteredFiledBasedRoutes = fileBasedRoutes.filter((fileBasedRoute) => { diff --git a/packages/astro/src/vite-plugin-astro-server/base.ts b/packages/astro/src/vite-plugin-astro-server/base.ts index 562b89ba22040..89fc13805f480 100644 --- a/packages/astro/src/vite-plugin-astro-server/base.ts +++ b/packages/astro/src/vite-plugin-astro-server/base.ts @@ -9,6 +9,8 @@ import type { Logger } from '../core/logger/core.js'; import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js'; import { writeHtmlResponse } from './response.js'; +const justSlashes = /^\/{2,}$/; + export function baseMiddleware( settings: AstroSettings, logger: Logger, @@ -24,7 +26,7 @@ export function baseMiddleware( let pathname: string; try { - pathname = decodeURI(new URL(url, 'http://localhost').pathname); + pathname = justSlashes.test(url) ? '//' : decodeURI(new URL(url, 'http://localhost').pathname); } catch (e) { /* malform uri */ return next(e); diff --git a/packages/astro/test/dev-routing.test.js b/packages/astro/test/dev-routing.test.js index c6a19fc4e4408..a43561769a1a6 100644 --- a/packages/astro/test/dev-routing.test.js +++ b/packages/astro/test/dev-routing.test.js @@ -48,6 +48,19 @@ describe('Development Routing', () => { assert.equal(response.status, 200); }); + it('redirects when loading double slash', async () => { + const response = await fixture.fetch('//', { redirect: 'manual' }); + assert.equal(response.status, 301); + assert.equal(response.headers.get('Location'), '/'); + }); + + it('redirects when loading multiple slashes', async () => { + const response = await fixture.fetch('/////', { redirect: 'manual' }); + assert.equal(response.status, 301); + assert.equal(response.headers.get('Location'), '/'); + }); + + it('404 when loading invalid dynamic route', async () => { const response = await fixture.fetch('/2'); assert.equal(response.status, 404);