From 2c9f2f06331891c19d0031a70cbff0b196815928 Mon Sep 17 00:00:00 2001 From: "factory[bot]" Date: Sun, 4 Oct 2026 18:22:38 +0000 Subject: [PATCH] fix(astro): resolve context.props when actions() or pages() run before middleware() --- .changeset/fix-actions-props-null.md | 5 +++ packages/astro/src/core/fetch/fetch-state.ts | 14 +++++--- packages/astro/src/core/pages/handler.ts | 3 +- packages/astro/test/units/fetch/index.test.ts | 35 +++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-actions-props-null.md diff --git a/.changeset/fix-actions-props-null.md b/.changeset/fix-actions-props-null.md new file mode 100644 index 000000000000..6d4d4201f912 --- /dev/null +++ b/.changeset/fix-actions-props-null.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes `context.props` being `null` in middleware and endpoints when the composable `astro/hono` or `astro/fetch` `actions()` handler runs before `middleware()`, or when `pages()` runs without `middleware()` diff --git a/packages/astro/src/core/fetch/fetch-state.ts b/packages/astro/src/core/fetch/fetch-state.ts index 0c0e62e3c478..f7dbcd915257 100644 --- a/packages/astro/src/core/fetch/fetch-state.ts +++ b/packages/astro/src/core/fetch/fetch-state.ts @@ -1228,8 +1228,10 @@ export class FetchState implements AstroFetchState { * Returns the `APIContext` for this render, creating it lazily from * the memoized props + action context. * - * Callers must ensure `getProps()` has resolved at least once before - * calling this. + * Called before `getProps()` has resolved, the context carries + * `props: null` and is not memoized; the next call after `getProps()` + * fills in `props` on the same object. Callers that read `props` + * must await `getProps()` first. */ getAPIContext(): APIContext { if (this.apiContext !== null) return this.apiContext; @@ -1246,14 +1248,18 @@ export class FetchState implements AstroFetchState { (actionApiContext as any)[fetchStateSymbol] = this; - this.apiContext = Object.assign(actionApiContext, { + const apiContext = Object.assign(actionApiContext, { props: this.props!, redirect, rewrite, getActionResult: createGetActionResult(actionApiContext.locals), callAction: createCallAction(actionApiContext), }); - return this.apiContext; + // A composable handler such as `actions()` can run before anything has + // awaited `getProps()`. Memoizing only resolved props lets the next call + // copy them onto the same context object (#18229). + if (this.props !== null) this.apiContext = apiContext; + return apiContext; } /** diff --git a/packages/astro/src/core/pages/handler.ts b/packages/astro/src/core/pages/handler.ts index 089297a4e51e..1eab7613dad9 100644 --- a/packages/astro/src/core/pages/handler.ts +++ b/packages/astro/src/core/pages/handler.ts @@ -122,7 +122,8 @@ export async function handlePagesWithErrorFallback(state: FetchState): Promise { assert.equal(final.status, 200); assert.match(await final.text(), /

Hello<\/h1>/); }); + + it('passes resolved props to middleware and endpoints when actions() runs before middleware()', async () => { + let middlewareProps: unknown; + const app = createTestApp( + [createEndpoint({ GET: ({ props }: any) => Response.json(props) }, { route: '/echo' })], + { + middleware: async () => ({ + onRequest: async (ctx: any, next: any) => { + middlewareProps = ctx.props; + return next(); + }, + }), + }, + ); + const request = stampApp(new Request('http://example.com/echo'), app); + const state = new FetchState(request); + + assert.equal(await actions(state), undefined); + const response = await middleware(state, () => pages(state)); + + assert.deepEqual(middlewareProps, {}); + assert.deepEqual(await response.json(), {}); + }); + + it('passes resolved props to endpoints when pages() runs without middleware()', async () => { + const app = createTestApp([ + createEndpoint({ GET: ({ props }: any) => Response.json(props) }, { route: '/echo' }), + ]); + const request = stampApp(new Request('http://example.com/echo'), app); + const state = new FetchState(request); + + const response = await pages(state); + + assert.deepEqual(await response.json(), {}); + }); }); // #endregion