From bdb75a87f24d7f032797483164fb2f82aa691fee Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 11 Nov 2024 15:03:27 +0000 Subject: [PATCH] fix(routing): emit error for forbidden rewrite (#12339) Co-authored-by: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com> Co-authored-by: Sarah Rainsberger Co-authored-by: Bjorn Lu Co-authored-by: Florian Lefebvre Co-authored-by: Reuben Tier Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> --- .changeset/proud-games-repair.md | 7 ++++ packages/astro/src/core/errors/errors-data.ts | 17 ++++++++++ .../astro/src/core/middleware/sequence.ts | 18 +++++++++++ packages/astro/src/core/render-context.ts | 32 +++++++++++++++++++ .../src/pages/forbidden/dynamic.astro | 4 +++ .../src/pages/forbidden/static.astro | 3 ++ packages/astro/test/rewrite.test.js | 7 ++++ 7 files changed, 88 insertions(+) create mode 100644 .changeset/proud-games-repair.md create mode 100644 packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/dynamic.astro create mode 100644 packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/static.astro diff --git a/.changeset/proud-games-repair.md b/.changeset/proud-games-repair.md new file mode 100644 index 000000000000..8c342e2a3985 --- /dev/null +++ b/.changeset/proud-games-repair.md @@ -0,0 +1,7 @@ +--- +'astro': patch +--- + +Adds an error when `Astro.rewrite()` is used to rewrite an on-demand route with a static route when using the `"server"` output. + +This is a forbidden rewrite because Astro can't retrieve the emitted static route at runtime. This route is served by the hosting platform, and not Astro itself. diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 6b3c7c141ab2..7c5479a665f5 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1260,6 +1260,23 @@ export const RewriteWithBodyUsed = { 'Astro.rewrite() cannot be used if the request body has already been read. If you need to read the body, first clone the request.', } satisfies ErrorData; +/** + * @docs + * @description + * `Astro.rewrite()` can't be used to rewrite an on-demand route with a static route when using the `"server"` output. + * + */ +export const ForbiddenRewrite = { + name: 'ForbiddenRewrite', + title: 'Forbidden rewrite to a static route.', + message: (from: string, to: string, component: string) => + `You tried to rewrite the on-demand route '${from}' with the static route '${to}', when using the 'server' output. \n\nThe static route '${to}' is rendered by the component +'${component}', which is marked as prerendered. This is a forbidden operation because during the build the component '${component}' is compiled to an +HTML file, which can't be retrieved at runtime by Astro.`, + hint: (component: string) => + `Add \`export const prerender = false\` to the component '${component}', or use a Astro.redirect().`, +} satisfies ErrorData; + /** * @docs * @description diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 1fbba7c6600c..99d506695ea3 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,6 +1,8 @@ import type { MiddlewareHandler, RewritePayload } from '../../types/public/common.js'; import type { APIContext } from '../../types/public/context.js'; import { AstroCookies } from '../cookies/cookies.js'; +import { ForbiddenRewrite } from '../errors/errors-data.js'; +import { AstroError } from '../errors/index.js'; import { apiContextRoutesSymbol } from '../render-context.js'; import { type Pipeline, getParams } from '../render/index.js'; import { defineMiddleware } from './index.js'; @@ -49,6 +51,22 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { payload, handleContext.request, ); + + // This is a case where the user tries to rewrite from a SSR route to a prerendered route (SSG). + // This case isn't valid because when building for SSR, the prerendered route disappears from the server output because it becomes an HTML file, + // so Astro can't retrieve it from the emitted manifest. + if ( + pipeline.serverLike === true && + handleContext.isPrerendered === false && + routeData.prerender === true + ) { + throw new AstroError({ + ...ForbiddenRewrite, + message: ForbiddenRewrite.message(pathname, pathname, routeData.component), + hint: ForbiddenRewrite.hint(routeData.component), + }); + } + carriedPayload = payload; handleContext.request = newRequest; handleContext.url = new URL(newRequest.url); diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 49f174c33f01..691613412e91 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -23,6 +23,7 @@ import { } from './constants.js'; import { AstroCookies, attachCookiesToResponse } from './cookies/index.js'; import { getCookiesFromResponse } from './cookies/response.js'; +import { ForbiddenRewrite } from './errors/errors-data.js'; import { AstroError, AstroErrorData } from './errors/index.js'; import { callMiddleware } from './middleware/callMiddleware.js'; import { sequence } from './middleware/index.js'; @@ -145,6 +146,22 @@ export class RenderContext { pathname, newUrl, } = await pipeline.tryRewrite(payload, this.request); + + // This is a case where the user tries to rewrite from a SSR route to a prerendered route (SSG). + // This case isn't valid because when building for SSR, the prerendered route disappears from the server output because it becomes an HTML file, + // so Astro can't retrieve it from the emitted manifest. + if ( + this.pipeline.serverLike === true && + this.routeData.prerender === false && + routeData.prerender === true + ) { + throw new AstroError({ + ...ForbiddenRewrite, + message: ForbiddenRewrite.message(this.pathname, pathname, routeData.component), + hint: ForbiddenRewrite.hint(routeData.component), + }); + } + this.routeData = routeData; componentInstance = newComponent; if (payload instanceof Request) { @@ -246,6 +263,21 @@ export class RenderContext { reroutePayload, this.request, ); + // This is a case where the user tries to rewrite from a SSR route to a prerendered route (SSG). + // This case isn't valid because when building for SSR, the prerendered route disappears from the server output because it becomes an HTML file, + // so Astro can't retrieve it from the emitted manifest. + if ( + this.pipeline.serverLike === true && + this.routeData.prerender === false && + routeData.prerender === true + ) { + throw new AstroError({ + ...ForbiddenRewrite, + message: ForbiddenRewrite.message(this.pathname, pathname, routeData.component), + hint: ForbiddenRewrite.hint(routeData.component), + }); + } + this.routeData = routeData; if (reroutePayload instanceof Request) { this.request = reroutePayload; diff --git a/packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/dynamic.astro b/packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/dynamic.astro new file mode 100644 index 000000000000..3c23c9539196 --- /dev/null +++ b/packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/dynamic.astro @@ -0,0 +1,4 @@ +--- +return Astro.rewrite("/forbidden/static") +export const prerender = false +--- diff --git a/packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/static.astro b/packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/static.astro new file mode 100644 index 000000000000..3a91bda712f1 --- /dev/null +++ b/packages/astro/test/fixtures/rewrite-server/src/pages/forbidden/static.astro @@ -0,0 +1,3 @@ +--- +export const prerender = true +--- diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js index 273e11d0730d..cc7508081092 100644 --- a/packages/astro/test/rewrite.test.js +++ b/packages/astro/test/rewrite.test.js @@ -149,6 +149,13 @@ describe('Dev rewrite, hybrid/server', () => { assert.equal($('title').text(), 'RewriteWithBodyUsed'); }); + + it('should error when rewriting from a SSR route to a SSG route', async () => { + const html = await fixture.fetch('/forbidden/dynamic').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.match($('title').text(), /ForbiddenRewrite/); + }); }); describe('Build reroute', () => {