fix(ssr): correctly call the middleware when rendering error pages (#8200)
This commit is contained in:
parent
660e6f80f4
commit
46d0a0b006
13 changed files with 81 additions and 26 deletions
|
@ -9,7 +9,7 @@ import type {
|
|||
import type { SinglePageBuiltModule } from '../build/types';
|
||||
import { getSetCookiesFromResponse } from '../cookies/index.js';
|
||||
import { consoleLogDestination } from '../logger/console.js';
|
||||
import { error, type LogOptions } from '../logger/core.js';
|
||||
import { error, type LogOptions, warn } from '../logger/core.js';
|
||||
import {
|
||||
collapseDuplicateSlashes,
|
||||
prependForwardSlash,
|
||||
|
@ -56,6 +56,8 @@ export class App {
|
|||
};
|
||||
#baseWithoutTrailingSlash: string;
|
||||
#pipeline: SSRRoutePipeline;
|
||||
#onRequest: MiddlewareEndpointHandler | undefined;
|
||||
#middlewareLoaded: boolean;
|
||||
|
||||
constructor(manifest: SSRManifest, streaming = true) {
|
||||
this.#manifest = manifest;
|
||||
|
@ -65,6 +67,7 @@ export class App {
|
|||
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
|
||||
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
|
||||
this.#pipeline = new SSRRoutePipeline(this.#createEnvironment(streaming));
|
||||
this.#middlewareLoaded = false;
|
||||
}
|
||||
|
||||
set setManifest(newManifest: SSRManifest) {
|
||||
|
@ -128,7 +131,21 @@ export class App {
|
|||
if (!routeData || routeData.prerender) return undefined;
|
||||
return routeData;
|
||||
}
|
||||
|
||||
async #getOnRequest() {
|
||||
if (this.#manifest.middlewareEntryPoint && !this.#middlewareLoaded) {
|
||||
try {
|
||||
const middleware = await import(this.#manifest.middlewareEntryPoint);
|
||||
this.#pipeline.setMiddlewareFunction(middleware.onRequest as MiddlewareEndpointHandler);
|
||||
} catch (e) {
|
||||
warn(this.#logging, 'SSR', "Couldn't load the middleware entry point");
|
||||
}
|
||||
}
|
||||
this.#middlewareLoaded = true;
|
||||
}
|
||||
|
||||
async render(request: Request, routeData?: RouteData, locals?: object): Promise<Response> {
|
||||
await this.#getOnRequest();
|
||||
// Handle requests with duplicate slashes gracefully by cloning with a cleaned-up request URL
|
||||
if (request.url !== collapseDuplicateSlashes(request.url)) {
|
||||
request = new Request(collapseDuplicateSlashes(request.url), request);
|
||||
|
@ -156,10 +173,6 @@ export class App {
|
|||
);
|
||||
let response;
|
||||
try {
|
||||
// NOTE: ideally we could set the middleware function just once, but we don't have the infrastructure to that yet
|
||||
if (mod.onRequest) {
|
||||
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
|
||||
}
|
||||
response = await this.#pipeline.renderRoute(renderContext, pageModule);
|
||||
} catch (err: any) {
|
||||
if (err instanceof EndpointNotFoundError) {
|
||||
|
|
|
@ -49,6 +49,7 @@ export type SSRManifest = {
|
|||
componentMetadata: SSRResult['componentMetadata'];
|
||||
pageModule?: SinglePageBuiltModule;
|
||||
pageMap?: Map<ComponentPath, ImportComponentInstance>;
|
||||
middlewareEntryPoint: string | undefined;
|
||||
};
|
||||
|
||||
export type SerializedSSRManifest = Omit<
|
||||
|
|
|
@ -77,6 +77,13 @@ export class BuildPipeline extends Pipeline {
|
|||
return this.#manifest;
|
||||
}
|
||||
|
||||
async retrieveMiddlewareFunction() {
|
||||
if (this.#internals.middlewareEntryPoint) {
|
||||
const middleware = await import(this.#internals.middlewareEntryPoint.toString());
|
||||
this.setMiddlewareFunction(middleware.onRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The SSR build emits two important files:
|
||||
* - dist/server/manifest.mjs
|
||||
|
|
|
@ -10,7 +10,6 @@ import type {
|
|||
ComponentInstance,
|
||||
GetStaticPathsItem,
|
||||
ImageTransform,
|
||||
MiddlewareEndpointHandler,
|
||||
RouteData,
|
||||
RouteType,
|
||||
SSRError,
|
||||
|
@ -138,6 +137,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
|
|||
);
|
||||
}
|
||||
const buildPipeline = new BuildPipeline(opts, internals, manifest);
|
||||
await buildPipeline.retrieveMiddlewareFunction();
|
||||
const outFolder = ssr
|
||||
? opts.settings.config.build.server
|
||||
: getOutDirWithinCwd(opts.settings.config.outDir);
|
||||
|
@ -248,10 +248,6 @@ async function generatePage(
|
|||
.reduce(mergeInlineCss, []);
|
||||
|
||||
const pageModulePromise = ssrEntry.page;
|
||||
const onRequest = ssrEntry.onRequest;
|
||||
if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
|
||||
}
|
||||
|
||||
if (!pageModulePromise) {
|
||||
throw new Error(
|
||||
|
@ -612,5 +608,8 @@ export function createBuildManifest(
|
|||
? new URL(settings.config.base, settings.config.site).toString()
|
||||
: settings.config.site,
|
||||
componentMetadata: internals.componentMetadata,
|
||||
middlewareEntryPoint: internals.middlewareEntryPoint
|
||||
? internals.middlewareEntryPoint.toString()
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -93,14 +93,19 @@ export function pluginManifest(
|
|||
}
|
||||
|
||||
const manifest = await createManifest(options, internals);
|
||||
const shouldPassMiddlewareEntryPoint =
|
||||
// TODO: remove in Astro 4.0
|
||||
options.settings.config.build.excludeMiddleware ||
|
||||
options.settings.adapter?.adapterFeatures?.edgeMiddleware;
|
||||
await runHookBuildSsr({
|
||||
config: options.settings.config,
|
||||
manifest,
|
||||
logging: options.logging,
|
||||
entryPoints: internals.entryPoints,
|
||||
middlewareEntryPoint: internals.middlewareEntryPoint,
|
||||
middlewareEntryPoint: shouldPassMiddlewareEntryPoint
|
||||
? internals.middlewareEntryPoint
|
||||
: undefined,
|
||||
});
|
||||
// TODO: use the manifest entry chunk instead
|
||||
const code = injectManifest(manifest, internals.manifestEntryChunk);
|
||||
mutate(internals.manifestEntryChunk, 'server', code);
|
||||
},
|
||||
|
@ -232,6 +237,10 @@ function buildManifest(
|
|||
// Set this to an empty string so that the runtime knows not to try and load this.
|
||||
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = '';
|
||||
}
|
||||
const isEdgeMiddleware =
|
||||
// TODO: remove in Astro 4.0
|
||||
settings.config.build.excludeMiddleware ||
|
||||
settings.adapter?.adapterFeatures?.edgeMiddleware;
|
||||
|
||||
const ssrManifest: SerializedSSRManifest = {
|
||||
adapterName: opts.settings.adapter?.name ?? '',
|
||||
|
@ -245,6 +254,7 @@ function buildManifest(
|
|||
clientDirectives: Array.from(settings.clientDirectives),
|
||||
entryModules,
|
||||
assets: staticFiles.map(prefixAssetPath),
|
||||
middlewareEntryPoint: !isEdgeMiddleware ? internals.middlewareEntryPoint?.toString() : undefined,
|
||||
};
|
||||
|
||||
return ssrManifest;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { addRollupInput } from '../add-rollup-input.js';
|
|||
import type { BuildInternals } from '../internal';
|
||||
import type { AstroBuildPlugin } from '../plugin';
|
||||
import type { StaticBuildOptions } from '../types';
|
||||
import { getOutputDirectory } from '../../../prerender/utils.js';
|
||||
|
||||
export const MIDDLEWARE_MODULE_ID = '@astro-middleware';
|
||||
|
||||
|
@ -56,8 +57,9 @@ export function vitePluginMiddleware(
|
|||
if (chunk.type === 'asset') {
|
||||
continue;
|
||||
}
|
||||
if (chunk.fileName === 'middleware.mjs' && opts.settings.config.build.excludeMiddleware) {
|
||||
internals.middlewareEntryPoint = new URL(chunkName, opts.settings.config.build.server);
|
||||
if (chunk.fileName === 'middleware.mjs') {
|
||||
const outputDirectory = getOutputDirectory(opts.settings.config);
|
||||
internals.middlewareEntryPoint = new URL(chunkName, outputDirectory);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@ import type {
|
|||
AstroSettings,
|
||||
ComponentInstance,
|
||||
ManifestData,
|
||||
MiddlewareHandler,
|
||||
RouteData,
|
||||
RuntimeMode,
|
||||
SSRLoadedRenderer,
|
||||
|
@ -52,7 +51,6 @@ export interface SinglePageBuiltModule {
|
|||
/**
|
||||
* The `onRequest` hook exported by the middleware
|
||||
*/
|
||||
onRequest?: MiddlewareHandler<unknown>;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,5 @@ export const RedirectComponentInstance: ComponentInstance = {
|
|||
|
||||
export const RedirectSinglePageBuiltModule: SinglePageBuiltModule = {
|
||||
page: () => Promise.resolve(RedirectComponentInstance),
|
||||
onRequest: (ctx, next) => next(),
|
||||
renderers: [],
|
||||
};
|
||||
|
|
|
@ -99,5 +99,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
|
|||
? new URL(settings.config.base, settings.config.site).toString()
|
||||
: settings.config.site,
|
||||
componentMetadata: new Map(),
|
||||
middlewareEntryPoint: undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,18 +18,20 @@ const first = defineMiddleware(async (context, next) => {
|
|||
return new Response(JSON.stringify(object), {
|
||||
headers: response.headers,
|
||||
});
|
||||
} else if(context.url.pathname === '/clone') {
|
||||
} else if (context.url.pathname === '/clone') {
|
||||
const response = await next();
|
||||
const newResponse = response.clone();
|
||||
const /** @type {string} */ html = await newResponse.text();
|
||||
const newhtml = html.replace('<h1>testing</h1>', '<h1>it works</h1>');
|
||||
return new Response(newhtml, { status: 200, headers: response.headers });
|
||||
} else {
|
||||
if(context.url.pathname === '/') {
|
||||
if (context.url.pathname === '/') {
|
||||
context.cookies.set('foo', 'bar');
|
||||
}
|
||||
|
||||
context.locals.name = 'bar';
|
||||
context.locals = {
|
||||
name: 'bar',
|
||||
};
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
|
|
6
packages/astro/test/fixtures/middleware-dev/src/pages/404.astro
vendored
Normal file
6
packages/astro/test/fixtures/middleware-dev/src/pages/404.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
const name = Astro.locals.name;
|
||||
---
|
||||
|
||||
<title>Error</title>
|
||||
<p>{name}</p>
|
|
@ -211,6 +211,16 @@ describe('Middleware API in PROD mode, SSR', () => {
|
|||
expect(text.includes('REDACTED')).to.be.true;
|
||||
});
|
||||
|
||||
it('should correctly call the middleware function for 404', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/funky-url');
|
||||
const routeData = app.match(request, { matchNotFound: true });
|
||||
const response = await app.render(request, routeData);
|
||||
const text = await response.text();
|
||||
expect(text.includes('Error')).to.be.true;
|
||||
expect(text.includes('bar')).to.be.true;
|
||||
});
|
||||
|
||||
it('the integration should receive the path to the middleware', async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/middleware-dev/',
|
||||
|
@ -219,10 +229,8 @@ describe('Middleware API in PROD mode, SSR', () => {
|
|||
excludeMiddleware: true,
|
||||
},
|
||||
adapter: testAdapter({
|
||||
setEntryPoints(entryPointsOrMiddleware) {
|
||||
if (entryPointsOrMiddleware instanceof URL) {
|
||||
setMiddlewareEntryPoint(entryPointsOrMiddleware) {
|
||||
middlewarePath = entryPointsOrMiddleware;
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
@ -237,6 +245,7 @@ describe('Middleware API in PROD mode, SSR', () => {
|
|||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Middleware with tailwind', () => {
|
||||
|
|
|
@ -5,7 +5,13 @@ import { viteID } from '../dist/core/util.js';
|
|||
* @returns {import('../src/@types/astro').AstroIntegration}
|
||||
*/
|
||||
export default function (
|
||||
{ provideAddress = true, extendAdapter, setEntryPoints = undefined, setRoutes = undefined } = {
|
||||
{
|
||||
provideAddress = true,
|
||||
extendAdapter,
|
||||
setEntryPoints = undefined,
|
||||
setMiddlewareEntryPoint = undefined,
|
||||
setRoutes = undefined,
|
||||
} = {
|
||||
provideAddress: true,
|
||||
}
|
||||
) {
|
||||
|
@ -86,7 +92,9 @@ export default function (
|
|||
'astro:build:ssr': ({ entryPoints, middlewareEntryPoint }) => {
|
||||
if (setEntryPoints) {
|
||||
setEntryPoints(entryPoints);
|
||||
setEntryPoints(middlewareEntryPoint);
|
||||
}
|
||||
if (setMiddlewareEntryPoint) {
|
||||
setMiddlewareEntryPoint(middlewareEntryPoint);
|
||||
}
|
||||
},
|
||||
'astro:build:done': ({ routes }) => {
|
||||
|
|
Loading…
Reference in a new issue