From 8797754d0b9c07ae7f6f53a48bc223f0e0e489ca Mon Sep 17 00:00:00 2001 From: "astro-factory[bot]" <316791938+astro-factory[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:12:11 +0100 Subject: [PATCH] Fix i18n fallback rewrite returning 500 instead of 404 for missing prerendered rest-param routes (#17799) Co-authored-by: factory[bot] --- .changeset/strong-lines-sit.md | 5 ++ packages/astro/src/core/i18n/handler.ts | 10 ++- .../astro/test/units/i18n/i18n-app.test.ts | 62 ++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .changeset/strong-lines-sit.md diff --git a/.changeset/strong-lines-sit.md b/.changeset/strong-lines-sit.md new file mode 100644 index 000000000000..fbb08da55ce8 --- /dev/null +++ b/.changeset/strong-lines-sit.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes i18n `fallbackType: "rewrite"` returning 500 instead of 404 when the fallback locale also has no matching static path for a prerendered dynamic route diff --git a/packages/astro/src/core/i18n/handler.ts b/packages/astro/src/core/i18n/handler.ts index 31497198a663..3e5563f8422c 100644 --- a/packages/astro/src/core/i18n/handler.ts +++ b/packages/astro/src/core/i18n/handler.ts @@ -180,7 +180,15 @@ export async function finalizeI18n( headers: { Location: fallbackDecision.pathname + url.search }, }); case 'rewrite': - return await state.rewrite(fallbackDecision.pathname + url.search); + try { + return await state.rewrite(fallbackDecision.pathname + url.search); + } catch { + // The rewrite target has no renderable page either (e.g. a + // prerendered dynamic route where the slug is excluded from + // getStaticPaths in the fallback locale too). Fall through + // so the original 404 response is returned. See #17778. + break; + } case 'none': break; } diff --git a/packages/astro/test/units/i18n/i18n-app.test.ts b/packages/astro/test/units/i18n/i18n-app.test.ts index c172155e6d5f..fdd408408271 100644 --- a/packages/astro/test/units/i18n/i18n-app.test.ts +++ b/packages/astro/test/units/i18n/i18n-app.test.ts @@ -5,7 +5,10 @@ import type { RoutingStrategies } from '../../../dist/core/app/common.js'; import { createI18nMiddleware } from '../../../dist/i18n/middleware.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; import type { Locales } from '../../../dist/types/public/config.js'; -import { createPage, createTestApp } from '../mocks.ts'; +import { createPage, createTestApp, createRouteData } from '../mocks.ts'; +import { createManifest, createRouteInfo } from '../app/test-helpers.ts'; +import { App } from '../../../dist/core/app/app.js'; +import type { SSRManifest, RouteInfo } from '../../../dist/core/app/types.js'; import { dynamicPart, spreadPart, staticPart } from '../routing/test-helpers.ts'; interface I18nConfigOverrides { @@ -666,3 +669,60 @@ describe('i18n via AstroHandler (no middleware) - prefix-always (#16800)', () => assert.equal(res.status, 404); }); }); + +// #17778: i18n fallback rewrite returns 404 (not 500) when the fallback target +// is a prerendered route whose component is not available at runtime. +describe('i18n fallback rewrite with prerendered routes not in pageMap', () => { + it('returns 404 when fallback rewrite targets a prerendered route with no runtime component', async () => { + const i18n = makeI18nConfig({ + defaultLocale: 'en', + locales: ['en', 'es'], + strategy: 'pathname-prefix-other-locales' as RoutingStrategies, + fallbackType: 'rewrite', + fallback: { es: 'en' }, + }); + + // Build a manifest where the prerendered blog routes appear in + // manifest.routes (so findRouteToRewrite can match them) but NOT + // in pageMap (simulating the production runtime where prerendered + // components are not bundled into the server entry). + const indexPage = createPage(localePage, { + route: '/', + component: 'src/pages/index.astro', + }); + const enBlogRoute = createRouteData({ + route: '/blog/[...slug]', + component: 'src/pages/blog/[...slug].astro', + prerender: true, + pathname: undefined, + segments: [[staticPart('blog')], [spreadPart('slug')]], + }); + const esBlogRoute = createRouteData({ + route: '/es/blog/[...slug]', + component: 'src/pages/es/blog/[...slug].astro', + prerender: true, + pathname: undefined, + segments: [[staticPart('es')], [staticPart('blog')], [spreadPart('slug')]], + }); + + const routes: RouteInfo[] = [ + createRouteInfo(indexPage.routeData) as RouteInfo, + createRouteInfo(enBlogRoute) as RouteInfo, + createRouteInfo(esBlogRoute) as RouteInfo, + ]; + // Only the SSR index page goes into pageMap; prerendered routes + // are excluded, matching real production behavior. + const pageMap = new Map Promise>>(); + pageMap.set(indexPage.routeData.component, indexPage.module); + + const manifest = createManifest({ + routes, + pageMap: pageMap as unknown as SSRManifest['pageMap'], + i18n, + }); + + const app = new App(manifest as unknown as SSRManifest); + const res = await app.render(new Request('http://example.com/es/blog/missing-post')); + assert.equal(res.status, 404, 'should return 404, not 500'); + }); +});