diff --git a/.changeset/gold-bees-enjoy.md b/.changeset/gold-bees-enjoy.md new file mode 100644 index 000000000000..59f64bb6b768 --- /dev/null +++ b/.changeset/gold-bees-enjoy.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +TODO diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index f0c600a40623..74000ff6d063 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -1,6 +1,7 @@ import { posix } from 'node:path'; import type { AstroConfig, + AstroMiddlewareInstance, AstroUserConfig, ComponentInstance, ContainerImportRendererFn, @@ -26,6 +27,7 @@ import { getParts, validateSegment } from '../core/routing/manifest/create.js'; import { getPattern } from '../core/routing/manifest/pattern.js'; import type { AstroComponentFactory } from '../runtime/server/index.js'; import { ContainerPipeline } from './pipeline.js'; +import { NOOP_MIDDLEWARE_FN } from '../core/middleware/noop-middleware.js'; /** * Options to be passed when rendering a route @@ -108,9 +110,11 @@ function createManifest( renderers?: SSRLoadedRenderer[], middleware?: MiddlewareHandler, ): SSRManifest { - const defaultMiddleware: MiddlewareHandler = (_, next) => { - return next(); - }; + function middlewareInstance(): AstroMiddlewareInstance { + return { + onRequest: middleware ?? NOOP_MIDDLEWARE_FN, + }; + } return { hrefRoot: import.meta.url, @@ -129,7 +133,7 @@ function createManifest( inlinedScripts: manifest?.inlinedScripts ?? new Map(), i18n: manifest?.i18n, checkOrigin: false, - middleware: manifest?.middleware ?? middleware ?? defaultMiddleware, + middleware: manifest?.middleware ?? middlewareInstance, experimentalEnvGetSecretEnabled: false, key: createKey(), }; @@ -475,11 +479,13 @@ export class experimental_AstroContainer { params: options.params, type: routeType, }); - const renderContext = RenderContext.create({ + const middlewareInstance = await this.#pipeline.middleware(); + const middleware = middlewareInstance.onRequest; + const renderContext = await RenderContext.create({ pipeline: this.#pipeline, routeData, status: 200, - middleware: this.#pipeline.middleware, + middleware, request, pathname: url.pathname, locals: options?.locals ?? {}, diff --git a/packages/astro/src/container/pipeline.ts b/packages/astro/src/container/pipeline.ts index 6a2af65ce384..8e4a04d7ceca 100644 --- a/packages/astro/src/container/pipeline.ts +++ b/packages/astro/src/container/pipeline.ts @@ -1,5 +1,6 @@ import type { ComponentInstance, + MiddlewareHandler, RewritePayload, RouteData, SSRElement, @@ -12,6 +13,9 @@ import { createStylesheetElementSet, } from '../core/render/ssr-element.js'; import { findRouteToRewrite } from '../core/routing/rewrite.js'; +import { NOOP_MIDDLEWARE_FN } from '../core/middleware/noop-middleware.js'; +import { sequence } from '../core/middleware/index.js'; +import { createOriginCheckMiddleware } from '../core/app/middlewares.js'; export class ContainerPipeline extends Pipeline { /** @@ -23,6 +27,23 @@ export class ContainerPipeline extends Pipeline { SinglePageBuiltModule >(); + resolvedMiddleware: MiddlewareHandler | undefined = undefined; + + async getMiddleware(): Promise { + if (this.resolvedMiddleware) { + return this.resolvedMiddleware; + } else { + const middlewareInstance = await this.middleware(); + const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN; + if (this.manifest.checkOrigin) { + this.resolvedMiddleware = sequence(createOriginCheckMiddleware(), onRequest); + } else { + this.resolvedMiddleware = onRequest; + } + return this.resolvedMiddleware; + } + } + static create({ logger, manifest, @@ -88,7 +109,7 @@ export class ContainerPipeline extends Pipeline { return Promise.resolve(componentInstance); }, renderers: this.manifest.renderers, - onRequest: this.manifest.middleware, + onRequest: this.resolvedMiddleware, }); } diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 7cfe1c5dd741..3d951267caeb 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -1,6 +1,7 @@ import { decodeKey } from '../encryption.js'; import { deserializeRouteData } from '../routing/manifest/serialization.js'; import type { RouteInfo, SSRManifest, SerializedSSRManifest } from './types.js'; +import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js'; export function deserializeManifest(serializedManifest: SerializedSSRManifest): SSRManifest { const routes: RouteInfo[] = []; @@ -23,8 +24,8 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): return { // in case user middleware exists, this no-op middleware will be reassigned (see plugin-ssr.ts) - middleware(_, next) { - return next(); + middleware() { + return { onRequest: NOOP_MIDDLEWARE_FN }; }, ...serializedManifest, assets, diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 8041dda3c6f7..6b606275aee9 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -24,6 +24,7 @@ import { createDefaultRoutes, injectDefaultRoutes } from '../routing/default.js' import { matchRoute } from '../routing/match.js'; import { createOriginCheckMiddleware } from './middlewares.js'; import { AppPipeline } from './pipeline.js'; +import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js'; export { deserializeManifest } from './common.js'; @@ -323,7 +324,7 @@ export class App { // Load route module. We also catch its error here if it fails on initialization const mod = await this.#pipeline.getModuleForRoute(routeData); - const renderContext = RenderContext.create({ + const renderContext = await RenderContext.create({ pipeline: this.#pipeline, locals, pathname, @@ -428,10 +429,10 @@ export class App { } const mod = await this.#pipeline.getModuleForRoute(errorRouteData); try { - const renderContext = RenderContext.create({ + const renderContext = await RenderContext.create({ locals, pipeline: this.#pipeline, - middleware: skipMiddleware ? (_, next) => next() : undefined, + middleware: skipMiddleware ? NOOP_MIDDLEWARE_FN : undefined, pathname: this.#getPathnameFromRequest(request), request, routeData: errorRouteData, diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 5ae5b775dde0..0792adacf5b6 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,6 +1,7 @@ import type { ComponentInstance, ManifestData, + MiddlewareHandler, RewritePayload, RouteData, SSRElement, @@ -11,9 +12,28 @@ import type { SinglePageBuiltModule } from '../build/types.js'; import { RedirectSinglePageBuiltModule } from '../redirects/component.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js'; import { findRouteToRewrite } from '../routing/rewrite.js'; +import { sequence } from '../middleware/index.js'; +import { createOriginCheckMiddleware } from './middlewares.js'; +import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js'; export class AppPipeline extends Pipeline { #manifestData: ManifestData | undefined; + resolvedMiddleware: MiddlewareHandler | undefined = undefined; + + async getMiddleware(): Promise { + if (this.resolvedMiddleware) { + return this.resolvedMiddleware; + } else { + const middlewareInstance = await this.middleware(); + const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN; + if (this.manifest.checkOrigin) { + this.resolvedMiddleware = sequence(createOriginCheckMiddleware(), onRequest); + } else { + this.resolvedMiddleware = onRequest; + } + return this.resolvedMiddleware; + } + } static create( manifestData: ManifestData, diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 6a00ec0a7993..b86ea04788c3 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -7,6 +7,7 @@ import type { SSRLoadedRenderer, SSRResult, SerializedRouteData, + AstroMiddlewareInstance, } from '../../@types/astro.js'; import type { RoutingStrategies } from '../../i18n/utils.js'; import type { SinglePageBuiltModule } from '../build/types.js'; @@ -68,7 +69,7 @@ export type SSRManifest = { serverIslandNameMap?: Map; key: Promise; i18n: SSRManifestI18n | undefined; - middleware: MiddlewareHandler; + middleware: () => Promise | AstroMiddlewareInstance; checkOrigin: boolean; // TODO: remove experimental prefix experimentalEnvGetSecretEnabled: boolean; diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 2281c562dc30..e3221c1c0932 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -102,6 +102,12 @@ export abstract class Pipeline { * @param routeData */ abstract getComponentByRoute(routeData: RouteData): Promise; + + /** + * Resolves the middleware from the manifest, and returns the `onRequest` function. If `onRequest` isn't there, + * it returns a no-op function + */ + abstract getMiddleware(): Promise; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index b24fb17c416c..7cd2aa91e367 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -443,7 +443,12 @@ async function generatePath( logger, staticLike: true, }); - const renderContext = RenderContext.create({ pipeline, pathname, request, routeData: route }); + const renderContext = await RenderContext.create({ + pipeline, + pathname, + request, + routeData: route, + }); let body: string | Uint8Array; let response: Response; @@ -552,7 +557,11 @@ function createBuildManifest( componentMetadata: internals.componentMetadata, i18n: i18nManifest, buildFormat: settings.config.build.format, - middleware, + middleware() { + return { + onRequest: middleware, + }; + }, checkOrigin: settings.config.security?.checkOrigin ?? false, key, experimentalEnvGetSecretEnabled: false, diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index 414144359a8c..06af3b902509 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -1,5 +1,6 @@ import type { ComponentInstance, + MiddlewareHandler, RewritePayload, RouteData, SSRLoadedRenderer, @@ -27,6 +28,9 @@ import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js'; import { getPagesFromVirtualModulePageName, getVirtualModulePageName } from './plugins/util.js'; import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; +import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js'; +import { sequence } from '../middleware/index.js'; +import { createOriginCheckMiddleware } from '../app/middlewares.js'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. @@ -49,6 +53,19 @@ export class BuildPipeline extends Pipeline { : getOutDirWithinCwd(this.settings.config.outDir); } + resolvedMiddleware: MiddlewareHandler | undefined = undefined; + + async getMiddleware(): Promise { + if (this.resolvedMiddleware) { + return this.resolvedMiddleware; + } else { + const middlewareInstance = await this.middleware(); + const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN; + this.resolvedMiddleware = onRequest; + return this.resolvedMiddleware; + } + } + private constructor( readonly internals: BuildInternals, readonly manifest: SSRManifest, @@ -134,7 +151,11 @@ export class BuildPipeline extends Pipeline { const renderers = await import(renderersEntryUrl.toString()); const middleware = await import(new URL('middleware.mjs', baseDirectory).toString()) - .then((mod) => mod.onRequest) + .then((mod) => { + return function () { + return { onRequest: mod.onRequest }; + }; + }) // middleware.mjs is not emitted if there is no user middleware // in which case the import fails with ERR_MODULE_NOT_FOUND, and we fall back to a no-op middleware .catch(() => manifest.middleware); diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index e9eda1dc90c2..883cdee38831 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -291,12 +291,11 @@ function generateSSRCode(settings: AstroSettings, adapter: AstroAdapter, middlew const contents = [ settings.config.experimental.serverIslands ? '' : `const serverIslandMap = new Map()`, - edgeMiddleware ? `const middleware = (_, next) => next()` : '', `const _manifest = Object.assign(defaultManifest, {`, ` ${pageMap},`, ` serverIslandMap,`, ` renderers,`, - ` middleware`, + ` middleware: ${edgeMiddleware ? 'undefined' : `() => import("${middlewareId}")`}`, `});`, `const _args = ${adapter.args ? JSON.stringify(adapter.args, null, 4) : 'undefined'};`, adapter.exports diff --git a/packages/astro/src/core/middleware/noop-middleware.ts b/packages/astro/src/core/middleware/noop-middleware.ts new file mode 100644 index 000000000000..bf5f10d89054 --- /dev/null +++ b/packages/astro/src/core/middleware/noop-middleware.ts @@ -0,0 +1,3 @@ +import type { MiddlewareHandler } from '../../@types/astro.js'; + +export const NOOP_MIDDLEWARE_FN: MiddlewareHandler = (_, next) => next(); diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 6dfa10137556..a5f6993cddc3 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -36,6 +36,7 @@ import { callMiddleware } from './middleware/callMiddleware.js'; import { sequence } from './middleware/index.js'; import { renderRedirect } from './redirects/render.js'; import { type Pipeline, Slots, getParams, getProps } from './render/index.js'; +import { NOOP_MIDDLEWARE_FN } from './middleware/noop-middleware.js'; /** * Each request is rendered using a `RenderContext`. @@ -70,7 +71,7 @@ export class RenderContext { */ counter = 0; - static create({ + static async create({ locals = {}, middleware, pathname, @@ -80,11 +81,14 @@ export class RenderContext { status = 200, props, }: Pick & - Partial>): RenderContext { + Partial< + Pick + >): Promise { + const pipelineMiddleware = await pipeline.getMiddleware(); return new RenderContext( pipeline, locals, - sequence(...pipeline.internalMiddleware, middleware ?? pipeline.middleware), + sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware), pathname, request, routeData, diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 80073a54eb00..94849d631a76 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -4,6 +4,7 @@ import type { ComponentInstance, DevToolbarMetadata, ManifestData, + MiddlewareHandler, RewritePayload, RouteData, SSRElement, @@ -27,6 +28,9 @@ import { getStylesForURL } from './css.js'; import { getComponentMetadata } from './metadata.js'; import { createResolve } from './resolve.js'; import { getScriptsForURL } from './scripts.js'; +import { NOOP_MIDDLEWARE_FN } from '../core/middleware/noop-middleware.js'; +import { sequence } from '../core/middleware/index.js'; +import { createOriginCheckMiddleware } from '../core/app/middlewares.js'; export class DevPipeline extends Pipeline { // renderers are loaded on every request, @@ -34,6 +38,18 @@ export class DevPipeline extends Pipeline { override renderers = new Array(); manifestData: ManifestData | undefined; + resolvedMiddleware: MiddlewareHandler | undefined = undefined; + + async getMiddleware(): Promise { + if (this.resolvedMiddleware) { + return this.resolvedMiddleware; + } else { + const middlewareInstance = await this.middleware(); + const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN; + this.resolvedMiddleware = onRequest; + return this.resolvedMiddleware; + } + } componentInterner: WeakMap = new WeakMap< RouteData, diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 19ceed811727..6633fa800083 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -152,8 +152,12 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest checkOrigin: settings.config.security?.checkOrigin ?? false, experimentalEnvGetSecretEnabled: false, key: createKey(), - middleware(_, next) { - return next(); + middleware() { + return { + onRequest(_, next) { + return next(); + }, + }; }, }; } diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index aa70d5324582..9bd38f566dfc 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -194,7 +194,7 @@ export async function handleRoute({ const isPrerendered404 = matchedRoute.route.route === '/404' && matchedRoute.route.prerender; - renderContext = RenderContext.create({ + renderContext = await RenderContext.create({ locals, pipeline, pathname, @@ -231,7 +231,7 @@ export async function handleRoute({ req({ url: pathname, method: incomingRequest.method, - statusCode: isRewrite ? response.status : status ?? response.status, + statusCode: isRewrite ? response.status : (status ?? response.status), isRewrite, reqTime: timeEnd - timeStart, }),