fix(routing): compute APIContext.params when rewriting using next (#12031)

* fix(routing): compute `APIContext.params` when rewriting using `next`

* fix(routing): compute `APIContext.params` when rewriting using `next`
This commit is contained in:
Emanuele Stoppa 2024-09-19 13:57:47 +01:00 committed by GitHub
parent d3bd673392
commit 8c0cae6d1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 90 additions and 37 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a bug where the rewrite via `next(/*..*/)` inside a middleware didn't compute the new `APIContext.params`

View file

@ -90,11 +90,7 @@ export class AppPipeline extends Pipeline {
return module.page(); return module.page();
} }
async tryRewrite( async tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult> {
payload: RewritePayload,
request: Request,
_sourceRoute: RouteData,
): Promise<TryRewriteResult> {
const { newUrl, pathname, routeData } = findRouteToRewrite({ const { newUrl, pathname, routeData } = findRouteToRewrite({
payload, payload,
request, request,

View file

@ -89,13 +89,8 @@ export abstract class Pipeline {
* *
* @param {RewritePayload} rewritePayload The payload provided by the user * @param {RewritePayload} rewritePayload The payload provided by the user
* @param {Request} request The original request * @param {Request} request The original request
* @param {RouteData} sourceRoute The original `RouteData`
*/ */
abstract tryRewrite( abstract tryRewrite(rewritePayload: RewritePayload, request: Request): Promise<TryRewriteResult>;
rewritePayload: RewritePayload,
request: Request,
sourceRoute: RouteData,
): Promise<TryRewriteResult>;
/** /**
* Tells the pipeline how to retrieve a component give a `RouteData` * Tells the pipeline how to retrieve a component give a `RouteData`

View file

@ -92,6 +92,10 @@ export class BuildPipeline extends Pipeline {
); );
} }
getRoutes(): RouteData[] {
return this.options.manifest.routes;
}
static create({ static create({
internals, internals,
manifest, manifest,
@ -286,11 +290,7 @@ export class BuildPipeline extends Pipeline {
return module.page(); return module.page();
} }
async tryRewrite( async tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult> {
payload: RewritePayload,
request: Request,
_sourceRoute: RouteData,
): Promise<TryRewriteResult> {
const { routeData, pathname, newUrl } = findRouteToRewrite({ const { routeData, pathname, newUrl } = findRouteToRewrite({
payload, payload,
request, request,

View file

@ -1,6 +1,8 @@
import type { APIContext, MiddlewareHandler, RewritePayload } from '../../@types/astro.js'; import type { APIContext, MiddlewareHandler, RewritePayload } from '../../@types/astro.js';
import { AstroCookies } from '../cookies/cookies.js'; import { AstroCookies } from '../cookies/cookies.js';
import { defineMiddleware } from './index.js'; import { defineMiddleware } from './index.js';
import { getParams, type Pipeline } from '../render/index.js';
import { apiContextRoutesSymbol } from '../render-context.js';
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
/** /**
@ -15,7 +17,6 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
return next(); return next();
}); });
} }
return defineMiddleware((context, next) => { return defineMiddleware((context, next) => {
/** /**
* This variable is used to carry the rerouting payload across middleware functions. * This variable is used to carry the rerouting payload across middleware functions.
@ -28,7 +29,7 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
// @ts-expect-error // @ts-expect-error
// SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually // SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually
// doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`. // doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`.
const result = handle(handleContext, async (payload: RewritePayload) => { const result = handle(handleContext, async (payload?: RewritePayload) => {
if (i < length - 1) { if (i < length - 1) {
if (payload) { if (payload) {
let newRequest; let newRequest;
@ -42,10 +43,16 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
handleContext.request, handleContext.request,
); );
} }
const pipeline: Pipeline = Reflect.get(handleContext, apiContextRoutesSymbol);
const { routeData, pathname } = await pipeline.tryRewrite(
payload,
handleContext.request,
);
carriedPayload = payload; carriedPayload = payload;
handleContext.request = newRequest; handleContext.request = newRequest;
handleContext.url = new URL(newRequest.url); handleContext.url = new URL(newRequest.url);
handleContext.cookies = new AstroCookies(newRequest); handleContext.cookies = new AstroCookies(newRequest);
handleContext.params = getParams(routeData, pathname);
} }
return applyHandle(i + 1, handleContext); return applyHandle(i + 1, handleContext);
} else { } else {

View file

@ -37,14 +37,13 @@ import { sequence } from './middleware/index.js';
import { renderRedirect } from './redirects/render.js'; import { renderRedirect } from './redirects/render.js';
import { type Pipeline, Slots, getParams, getProps } from './render/index.js'; import { type Pipeline, Slots, getParams, getProps } from './render/index.js';
export const apiContextRoutesSymbol = Symbol.for('context.routes');
/** /**
* Each request is rendered using a `RenderContext`. * Each request is rendered using a `RenderContext`.
* It contains data unique to each request. It is responsible for executing middleware, calling endpoints, and rendering the page by gathering necessary data from a `Pipeline`. * It contains data unique to each request. It is responsible for executing middleware, calling endpoints, and rendering the page by gathering necessary data from a `Pipeline`.
*/ */
export class RenderContext { export class RenderContext {
// The first route that this instance of the context attempts to render
originalRoute: RouteData;
private constructor( private constructor(
readonly pipeline: Pipeline, readonly pipeline: Pipeline,
public locals: App.Locals, public locals: App.Locals,
@ -57,9 +56,7 @@ export class RenderContext {
public params = getParams(routeData, pathname), public params = getParams(routeData, pathname),
protected url = new URL(request.url), protected url = new URL(request.url),
public props: Props = {}, public props: Props = {},
) { ) {}
this.originalRoute = routeData;
}
/** /**
* A flag that tells the render content if the rewriting was triggered * A flag that tells the render content if the rewriting was triggered
@ -142,14 +139,24 @@ export class RenderContext {
if (payload) { if (payload) {
pipeline.logger.debug('router', 'Called rewriting to:', payload); pipeline.logger.debug('router', 'Called rewriting to:', payload);
// we intentionally let the error bubble up // we intentionally let the error bubble up
const { routeData, componentInstance: newComponent } = await pipeline.tryRewrite( const {
payload, routeData,
this.request, componentInstance: newComponent,
this.originalRoute, pathname,
); newUrl,
} = await pipeline.tryRewrite(payload, this.request);
this.routeData = routeData; this.routeData = routeData;
componentInstance = newComponent; componentInstance = newComponent;
if (payload instanceof Request) {
this.request = payload;
} else {
this.request = this.#copyRequest(newUrl, this.request);
}
this.isRewriting = true; this.isRewriting = true;
this.url = new URL(this.request.url);
this.cookies = new AstroCookies(this.request);
this.params = getParams(routeData, pathname);
this.pathname = pathname;
this.status = 200; this.status = 200;
} }
let response: Response; let response: Response;
@ -218,6 +225,7 @@ export class RenderContext {
const context = this.createActionAPIContext(); const context = this.createActionAPIContext();
const redirect = (path: string, status = 302) => const redirect = (path: string, status = 302) =>
new Response(null, { status, headers: { Location: path } }); new Response(null, { status, headers: { Location: path } });
Reflect.set(context, apiContextRoutesSymbol, this.pipeline);
return Object.assign(context, { return Object.assign(context, {
props, props,
@ -237,7 +245,6 @@ export class RenderContext {
const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite( const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite(
reroutePayload, reroutePayload,
this.request, this.request,
this.originalRoute,
); );
this.routeData = routeData; this.routeData = routeData;
if (reroutePayload instanceof Request) { if (reroutePayload instanceof Request) {

View file

@ -199,11 +199,7 @@ export class DevPipeline extends Pipeline {
} }
} }
async tryRewrite( async tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult> {
payload: RewritePayload,
request: Request,
_sourceRoute: RouteData,
): Promise<TryRewriteResult> {
if (!this.manifestData) { if (!this.manifestData) {
throw new Error('Missing manifest data. This is an internal error, please file an issue.'); throw new Error('Missing manifest data. This is an internal error, please file an issue.');
} }

View file

@ -1,4 +1,5 @@
import { sequence } from 'astro:middleware'; import { sequence } from 'astro:middleware';
import {defineMiddleware} from "astro/middleware";
let contextReroute = false; let contextReroute = false;
@ -22,16 +23,23 @@ export const second = async (context, next) => {
if (context.url.pathname.includes('/auth/params')) { if (context.url.pathname.includes('/auth/params')) {
return next('/?foo=bar'); return next('/?foo=bar');
} }
if (context.url.pathname.includes('/auth/astro-params')) {
return next('/auth/1234');
}
} }
return next(); return next();
}; };
export const third = async (context, next) => { export const third = defineMiddleware(async (context, next) => {
// just making sure that we are testing the change in context coming from `next()` // just making sure that we are testing the change in context coming from `next()`
if (context.url.pathname.startsWith('/') && contextReroute === false) { if (context.url.pathname.startsWith('/') && contextReroute === false) {
context.locals.auth = 'Third function called'; context.locals.auth = 'Third function called';
} }
if (context.params?.id === '1234') {
context.locals.auth = 'Params changed'
}
return next(); return next();
}; });
export const onRequest = sequence(first, second, third); export const onRequest = sequence(first, second, third);

View file

@ -0,0 +1,23 @@
---
export function getStaticPaths( ) {
return [{
params: {
id: "1234"
}
}]
}
const { id } = Astro.params;
const auth = Astro.locals.auth;
---
<html>
<head>
<title>Index with params</title>
</head>
<body>
<h1>Index with params</h1>
<p id="params">Param: {id}</p>
<p id="locals">Locals: {auth}</p>
</body>
</html>

View file

@ -0,0 +1,3 @@
---
---

View file

@ -1,4 +1,6 @@
--- ---
const { id } = Astro.params;
const auth = Astro.locals.auth;
--- ---
<html> <html>
<head> <head>
@ -6,5 +8,7 @@
</head> </head>
<body> <body>
<h1>Index with params</h1> <h1>Index with params</h1>
<p id="params">Param: {id}</p>
<p id="locals">Locals: {auth}</p>
</body> </body>
</html> </html>

View file

@ -388,6 +388,15 @@ describe('Middleware', () => {
assert.match($('h1').text(), /Index/); assert.match($('h1').text(), /Index/);
}); });
it('should render correctly compute the new params next("/auth/1234")', async () => {
const html = await fixture.fetch('/auth/astro-params').then((res) => res.text());
const $ = cheerioLoad(html);
assert.match($('h1').text(), /Index with params/);
assert.match($('#params').text(), /Param: 1234/);
assert.match($('#locals').text(), /Locals: Params changed/);
});
}); });
describe('Middleware with custom 404.astro and 500.astro', () => { describe('Middleware with custom 404.astro and 500.astro', () => {