From e9126d7c745c1d868ad3997a2b405f9844b30199 Mon Sep 17 00:00:00 2001 From: astrobot-houston Date: Fri, 29 May 2026 08:10:32 +0000 Subject: [PATCH 1/3] fix(fetch): resolve 404 fallback by route path in advancedRouting mode --- .../fix-advanced-routing-404-fallback.md | 7 +++++ packages/astro/src/core/fetch/fetch-state.ts | 7 ++--- packages/astro/test/units/fetch/index.test.ts | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-advanced-routing-404-fallback.md diff --git a/.changeset/fix-advanced-routing-404-fallback.md b/.changeset/fix-advanced-routing-404-fallback.md new file mode 100644 index 000000000000..c2de85546569 --- /dev/null +++ b/.changeset/fix-advanced-routing-404-fallback.md @@ -0,0 +1,7 @@ +--- +'astro': patch +--- + +Fixes a bug where `experimental.advancedRouting` with `astro/hono` handlers threw `TypeError: Cannot read properties of undefined (reading 'route')` for unmatched routes instead of rendering the custom 404 page. + +The 404 fallback in `FetchState.#resolveRouteData()` compared route components by bare filename (`'404.astro'`), but the built manifest stores the full relative path (`'src/pages/404.astro'`). The comparison never matched, leaving `routeData` undefined and crashing downstream handlers. Fixed by using the existing `getCustom404Route()` helper which matches by route path (`/404`). diff --git a/packages/astro/src/core/fetch/fetch-state.ts b/packages/astro/src/core/fetch/fetch-state.ts index afbbfc5eb552..5b5d9b3625ad 100644 --- a/packages/astro/src/core/fetch/fetch-state.ts +++ b/packages/astro/src/core/fetch/fetch-state.ts @@ -15,7 +15,6 @@ import { AstroCookies } from '../cookies/index.js'; import { type Pipeline, Slots } from '../render/index.js'; import { ASTRO_GENERATOR, - DEFAULT_404_COMPONENT, fetchStateSymbol, originPathnameSymbol, pipelineSymbol, @@ -36,7 +35,7 @@ import { Rewrites } from '../rewrites/handler.js'; import { isRoute404or500, isRouteServerIsland } from '../routing/match.js'; import { normalizeUrl } from '../util/normalized-url.js'; import { getOriginPathname, setOriginPathname } from '../routing/rewrite.js'; -import { routeHasHtmlExtension } from '../routing/helpers.js'; +import { getCustom404Route, routeHasHtmlExtension } from '../routing/helpers.js'; import type { ResolvedRenderOptions } from '../app/base.js'; import { getRenderOptions } from '../app/render-options.js'; import { getFirstForwardedValue, validateForwardedHeaders } from '../app/validate-headers.js'; @@ -820,9 +819,7 @@ export class FetchState implements AstroFetchState { // Fall back to a 404 route so middleware can still run. if (!this.routeData) { - this.routeData = pipeline.manifestData.routes.find( - (route) => route.component === '404.astro' || route.component === DEFAULT_404_COMPONENT, - ); + this.routeData = getCustom404Route(pipeline.manifestData); } if (!this.routeData) { pipeline.logger.debug('router', "Astro hasn't found routes that match " + this.request.url); diff --git a/packages/astro/test/units/fetch/index.test.ts b/packages/astro/test/units/fetch/index.test.ts index 9f2d6fb33409..3741e1b15d4f 100644 --- a/packages/astro/test/units/fetch/index.test.ts +++ b/packages/astro/test/units/fetch/index.test.ts @@ -79,6 +79,16 @@ describe('FetchState (astro/fetch)', () => { assert.equal(state.routeData!.route, '/[b_ssr]'); assert.equal(state.routeData!.prerender, false); }); + + it('falls back to the 404 route when no route matches', () => { + const notFoundPage = createPage(simplePage, { route: '/404' }); + const app = createTestApp([createPage(simplePage, { route: '/' }), notFoundPage]); + const request = stampApp(new Request('http://example.com/does-not-exist'), app); + const state = new FetchState(request); + + assert.ok(state.routeData, 'routeData should fall back to the 404 route'); + assert.equal(state.routeData!.route, '/404'); + }); }); // #endregion @@ -238,6 +248,24 @@ describe('pages()', () => { assert.match(text, /

Hello<\/h1>/); }); + it('renders the 404 page for unmatched routes instead of throwing', async () => { + const notFoundPage = createComponent((_result: any, _props: any, _slots: any) => { + return render`

Not Found

`; + }); + const app = createTestApp([ + createPage(simplePage, { route: '/' }), + createPage(notFoundPage, { route: '/404' }), + ]); + const request = stampApp(new Request('http://example.com/does-not-exist'), app); + const state = new FetchState(request); + + const response = await pages(state); + + assert.equal(response.status, 404); + const text = await response.text(); + assert.match(text, /

Not Found<\/h1>/); + }); + it('renders an endpoint', async () => { const app = createTestApp([ createEndpoint( From 872aace7e1af6a669b1f6ed73abe2228af26bc32 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 29 May 2026 14:17:53 +0100 Subject: [PATCH 2/3] Update .changeset/fix-advanced-routing-404-fallback.md --- .changeset/fix-advanced-routing-404-fallback.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/fix-advanced-routing-404-fallback.md b/.changeset/fix-advanced-routing-404-fallback.md index c2de85546569..4ec812e69c1b 100644 --- a/.changeset/fix-advanced-routing-404-fallback.md +++ b/.changeset/fix-advanced-routing-404-fallback.md @@ -3,5 +3,3 @@ --- Fixes a bug where `experimental.advancedRouting` with `astro/hono` handlers threw `TypeError: Cannot read properties of undefined (reading 'route')` for unmatched routes instead of rendering the custom 404 page. - -The 404 fallback in `FetchState.#resolveRouteData()` compared route components by bare filename (`'404.astro'`), but the built manifest stores the full relative path (`'src/pages/404.astro'`). The comparison never matched, leaving `routeData` undefined and crashing downstream handlers. Fixed by using the existing `getCustom404Route()` helper which matches by route path (`/404`). From 824134b3c214ab8364339714c57ee33798480626 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 29 May 2026 16:37:33 -0400 Subject: [PATCH 3/3] fix(fetch): skip prerendered 404 routes in FetchState fallback Related: d69f858475 ## Goal The getCustom404Route() fix in this PR correctly finds the 404 route by path instead of the broken component-name comparison. But when the 404 page is prerendered (export const prerender = true), setting it as routeData causes the pipeline to attempt SSR rendering of a page that was already built to static HTML at build time. This crashes with a 500 instead of serving the pre-built 404 page. ## Decisions - Guard the fallback with !custom404.prerender: prerendered 404 pages must go through the renderError -> prerenderedErrorPageFetch path, which reads the HTML from disk. Only SSR 404 routes should be set as routeData so middleware can run and the component renders at runtime. ## Changes - fetch-state.ts: Added prerender check so the 404 fallback only activates for SSR routes. Prerendered routes leave routeData unset, which triggers the error handler's disk-serving path downstream. --- packages/astro/src/core/fetch/fetch-state.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/core/fetch/fetch-state.ts b/packages/astro/src/core/fetch/fetch-state.ts index 5b5d9b3625ad..3b531477e8c4 100644 --- a/packages/astro/src/core/fetch/fetch-state.ts +++ b/packages/astro/src/core/fetch/fetch-state.ts @@ -819,7 +819,14 @@ export class FetchState implements AstroFetchState { // Fall back to a 404 route so middleware can still run. if (!this.routeData) { - this.routeData = getCustom404Route(pipeline.manifestData); + const custom404 = getCustom404Route(pipeline.manifestData); + // Only use SSR 404 routes here. Prerendered 404 pages are already + // built to static HTML, so the pipeline can't render them at + // runtime. Leaving routeData unset lets the error handler serve + // the pre-built page from disk instead. + if (custom404 && !custom404.prerender) { + this.routeData = custom404; + } } if (!this.routeData) { pipeline.logger.debug('router', "Astro hasn't found routes that match " + this.request.url);