Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(routing): emit error for forbidden rewrite #12339

Merged
merged 7 commits into from
Nov 11, 2024
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
7 changes: 7 additions & 0 deletions .changeset/proud-games-repair.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that there may eventually be other kinds of "forbidden" rewrites, so not sure whether you want to add more context like ForbiddenRewriteToStatic or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not. The kind of error would be the same, but we can customise the message based on the situation.

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
Expand Down
18 changes: 18 additions & 0 deletions packages/astro/src/core/middleware/sequence.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we could avoid the duplication across all the pipelines?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really depends on how you want to avoid the duplication. We can put the check in one single function, but I think the error should be thrown where we consume the check. What do you have in mind?

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);
Expand Down
32 changes: 32 additions & 0 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
return Astro.rewrite("/forbidden/static")
export const prerender = false
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
export const prerender = true
---
7 changes: 7 additions & 0 deletions packages/astro/test/rewrite.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading