Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-actions-props-null.md
Original file line number Diff line number Diff line change
@@ -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()`
14 changes: 10 additions & 4 deletions packages/astro/src/core/fetch/fetch-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/pages/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export async function handlePagesWithErrorFallback(state: FetchState): Promise<R
return createCrossOriginForbiddenResponse(ctx.request);
}
try {
return await handlePages(state, ctx);
await state.getProps();
return await handlePages(state, state.getAPIContext());
} catch (err: any) {
// The header marker can't carry the error object, so render the
// 500 page directly to preserve `error` and the logged stack.
Expand Down
35 changes: 35 additions & 0 deletions packages/astro/test/units/fetch/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,41 @@ describe('Composed pipeline', () => {
assert.equal(final.status, 200);
assert.match(await final.text(), /<h1>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
Expand Down
Loading