Skip to content

Commit

Permalink
feat: rerouting in the middleware (#10853)
Browse files Browse the repository at this point in the history
* feat: implement reroute in dev (#10818)

* chore: implement reroute in dev

* chore: revert naming change

* chore: conditionally create the new request

* chore: handle error

* remove only

* remove only

* chore: add tests and remove logs

* chore: fix regression

* chore: fix regression route matching

* chore: remove unwanted test

* feat: reroute in SSG (#10843)

* feat: rerouting in ssg

* linting

* feat: rerouting in ssg

* linting

* feat: reroute for SSR

* fix rebase

* fix merge issue

* feat: implement the `next(payload)` feature for rerouting

* chore: revert code

* chore: fix code

* Apply suggestions from code review

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>

---------

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
  • Loading branch information
ematipico and bluwy committed May 6, 2024
1 parent 8b2eb34 commit 51d9d9a
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 10 deletions.
5 changes: 5 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
8 changes: 6 additions & 2 deletions packages/astro/src/core/middleware/callMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ import { AstroError, AstroErrorData } from '../errors/index.js';
export async function callMiddleware(
onRequest: MiddlewareHandler,
apiContext: APIContext,
responseFunction: (reroutePayload?: ReroutePayload) => Promise<Response> | Response
responseFunction: (
apiContext: APIContext,
reroutePayload?: ReroutePayload
) => Promise<Response> | Response
): Promise<Response> {
let nextCalled = false;
let responseFunctionPromise: Promise<Response> | 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;
};

Expand Down
26 changes: 24 additions & 2 deletions packages/astro/src/core/middleware/sequence.ts
Original file line number Diff line number Diff line change
@@ -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
/**
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
8 changes: 3 additions & 5 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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': {
Expand Down Expand Up @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/test/fixtures/reroute/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
experimental: {
rerouting: true
}
},
site: "https://example.com"
});
33 changes: 33 additions & 0 deletions packages/astro/test/fixtures/reroute/src/middleware.js
Original file line number Diff line number Diff line change
@@ -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);
10 changes: 10 additions & 0 deletions packages/astro/test/fixtures/reroute/src/pages/auth/base.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---
<html>
<head>
<title>Base</title>
</head>
<body>
<h1>Base</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---
<html>
<head>
<title>Dashboard</title>
</head>
<body>
<h1>Dashboard</h1>
</body>
</html>
10 changes: 10 additions & 0 deletions packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---
<html>
<head>
<title>Settings</title>
</head>
<body>
<h1>Settings</h1>
</body>
</html>
2 changes: 2 additions & 0 deletions packages/astro/test/fixtures/reroute/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
---
const auth = Astro.locals.auth;
---
<html>
<head>
<title>Index</title>
</head>
<body>
<h1>Index</h1>
{auth ? <p>Called auth</p>: ""}
</body>
</html>
33 changes: 33 additions & 0 deletions packages/astro/test/reroute.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(), '');
});
});

0 comments on commit 51d9d9a

Please sign in to comment.