diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index cc9c2054166a..67b89ba48f2e 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2626,6 +2626,11 @@ export interface APIContext< */ redirect: AstroSharedContext['redirect']; + /** + * TODO: docs + */ + reroute: AstroSharedContext['reroute']; + /** * An object that middlewares can use to store extra information related to the request. * diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 5a0456680a6c..755291be39cb 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -43,13 +43,17 @@ import { AstroError, AstroErrorData } from '../errors/index.js'; export async function callMiddleware( onRequest: MiddlewareHandler, apiContext: APIContext, - responseFunction: (reroutePayload?: ReroutePayload) => Promise | Response + responseFunction: ( + apiContext: APIContext, + reroutePayload?: ReroutePayload + ) => Promise | Response ): Promise { let nextCalled = false; let responseFunctionPromise: Promise | Response | undefined = undefined; const next: MiddlewareNext = async (payload) => { nextCalled = true; - responseFunctionPromise = responseFunction(payload); + // We need to pass the APIContext pass to `callMiddleware` because it can be mutated across middleware functions + responseFunctionPromise = responseFunction(apiContext, payload); return responseFunctionPromise; }; diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 5a0842d8f842..3782bc30befa 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,5 +1,6 @@ import type { APIContext, MiddlewareHandler, ReroutePayload } from '../../@types/astro.js'; import { defineMiddleware } from './index.js'; +import { AstroCookies } from '../cookies/cookies.js'; // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js /** @@ -10,12 +11,16 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { const filtered = handlers.filter((h) => !!h); const length = filtered.length; if (!length) { - return defineMiddleware((context, next) => { + return defineMiddleware((_context, next) => { return next(); }); } return defineMiddleware((context, next) => { + /** + * This variable is used to carry the rerouting payload across middleware functions. + */ + let carriedPayload: ReroutePayload | undefined = undefined; return applyHandle(0, context); function applyHandle(i: number, handleContext: APIContext) { @@ -25,9 +30,26 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { // 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: ReroutePayload) => { if (i < length - 1) { + if (payload) { + let newRequest; + if (payload instanceof Request) { + newRequest = payload; + } else if (payload instanceof URL) { + newRequest = new Request(payload, handleContext.request); + } else { + newRequest = new Request( + new URL(payload, handleContext.url.origin), + handleContext.request + ); + } + carriedPayload = payload; + handleContext.request = newRequest; + handleContext.url = new URL(newRequest.url); + handleContext.cookies = new AstroCookies(newRequest); + } return applyHandle(i + 1, handleContext); } else { - return next(payload); + return next(payload ?? carriedPayload); } }); return result; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index bec6feba3a25..efada3e215a7 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -111,7 +111,7 @@ export class RenderContext { statusText: 'Loop Detected', }); } - const lastNext: MiddlewareNext = async (payload) => { + const lastNext = async (ctx: APIContext, payload?: ReroutePayload) => { if (payload) { if (this.pipeline.manifest.reroutingEnabled) { try { @@ -135,7 +135,7 @@ export class RenderContext { } switch (this.routeData.type) { case 'endpoint': - return renderEndpoint(componentInstance as any, apiContext, serverLike, logger); + return renderEndpoint(componentInstance as any, ctx, serverLike, logger); case 'redirect': return renderRedirect(this); case 'page': { @@ -174,9 +174,7 @@ export class RenderContext { } }; - const response = this.isRerouting - ? await lastNext() - : await callMiddleware(middleware, apiContext, lastNext); + const response = await callMiddleware(middleware, apiContext, lastNext); if (response.headers.get(ROUTE_TYPE_HEADER)) { response.headers.delete(ROUTE_TYPE_HEADER); } diff --git a/packages/astro/test/fixtures/reroute/astro.config.mjs b/packages/astro/test/fixtures/reroute/astro.config.mjs index af736916179e..6d20ec89a31d 100644 --- a/packages/astro/test/fixtures/reroute/astro.config.mjs +++ b/packages/astro/test/fixtures/reroute/astro.config.mjs @@ -4,5 +4,6 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ experimental: { rerouting: true - } + }, + site: "https://example.com" }); diff --git a/packages/astro/test/fixtures/reroute/src/middleware.js b/packages/astro/test/fixtures/reroute/src/middleware.js new file mode 100644 index 000000000000..0cf03c1d7713 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/middleware.js @@ -0,0 +1,33 @@ +import { sequence } from 'astro:middleware'; + +let contextReroute = false; + +export const first = async (context, next) => { + if (context.url.pathname.includes('/auth')) { + } + + return next(); +}; + +export const second = async (context, next) => { + if (context.url.pathname.includes('/auth')) { + if (context.url.pathname.includes('/auth/dashboard')) { + contextReroute = true; + return await context.reroute('/'); + } + if (context.url.pathname.includes('/auth/base')) { + return await next('/'); + } + } + return next(); +}; + +export const third = async (context, next) => { + // just making sure that we are testing the change in context coming from `next()` + if (context.url.pathname.startsWith('/') && contextReroute === false) { + context.locals.auth = 'Third function called'; + } + return next(); +}; + +export const onRequest = sequence(first, second, third); diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro new file mode 100644 index 000000000000..be31dfb14141 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro @@ -0,0 +1,10 @@ +--- +--- + + + Base + + +

Base

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro new file mode 100644 index 000000000000..bfa006aa01a7 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro @@ -0,0 +1,10 @@ +--- +--- + + + Dashboard + + +

Dashboard

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro new file mode 100644 index 000000000000..9eee5fe95149 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro @@ -0,0 +1,10 @@ +--- +--- + + + Settings + + +

Settings

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/index.astro b/packages/astro/test/fixtures/reroute/src/pages/index.astro index 727a45a65758..91a6fd0fb0fc 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/index.astro +++ b/packages/astro/test/fixtures/reroute/src/pages/index.astro @@ -1,4 +1,5 @@ --- +const auth = Astro.locals.auth; --- @@ -6,5 +7,6 @@

Index

+ {auth ?

Called auth

: ""} diff --git a/packages/astro/test/reroute.test.js b/packages/astro/test/reroute.test.js index ba0ea41bc31a..3a9273258e81 100644 --- a/packages/astro/test/reroute.test.js +++ b/packages/astro/test/reroute.test.js @@ -165,3 +165,36 @@ describe('SSR reroute', () => { assert.equal($('h1').text(), 'Index'); }); }); + +describe('Middleware', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('should render a locals populated in the third middleware function, because we use next("/")', async () => { + const html = await fixture.fetch('/auth/base').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + assert.equal($('p').text(), 'Called auth'); + }); + + it('should NOT render locals populated in the third middleware function, because we use ctx.reroute("/")', async () => { + const html = await fixture.fetch('/auth/dashboard').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + assert.equal($('p').text(), ''); + }); +});