withastro/astro · error · Error

FetchState not found on APIContext. `next(payload)` rewrites

Error message

FetchState not found on APIContext. `next(payload)` rewrites require a context created through Astro's request pipeline.

What it means

sequence()'s rewrite form — next('/path') — retrieves the FetchState that Astro's request pipeline stamps on the context under a private symbol. Contexts created any other way (notably createContext() contexts used by adapter edge-middleware bridges) never carry that symbol, so the rewrite cannot be routed and this Error is thrown: the context must come through Astro's request pipeline for payload rewrites.

Source

Thrown at packages/astro/src/core/middleware/sequence.ts:47

		/**
		 * This variable is used to carry the rerouting payload across middleware functions.
		 */
		let carriedPayload: RewritePayload | undefined = undefined;
		return applyHandle(0, context);

		function applyHandle(i: number, handleContext: APIContext) {
			const handle = filtered[i];
			// @ts-expect-error
			// SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually
			// 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?: RewritePayload) => {
				if (i < length - 1) {
					if (payload) {
						const oldPathname = handleContext.url.pathname;
						const state = Reflect.get(handleContext, fetchStateSymbol) as FetchState | undefined;
						if (!state) {
							// Outside Astro's request pipeline the state is never stamped.
							throw new Error(
								"FetchState not found on APIContext. `next(payload)` rewrites require a context created through Astro's request pipeline.",
							);
						}
						const manifest = state.manifest;
						const { routeData, pathname } = await getEnvironment(manifest).tryRewrite(
							manifest,
							payload,
							handleContext.request,
						);
						let newRequest: Request;
						if (payload instanceof Request) {
							newRequest = payload;
						} else {
							const request =
								handleContext.request.method === 'GET' || handleContext.request.method === 'HEAD'
									? handleContext.request
									: handleContext.request.clone();
							const newUrl =

View on GitHub (pinned to 157c500c38)

Solutions

  1. In edge middleware, use the platform's native rewrite mechanism instead of next('/path')
  2. Return a redirect Response (context.redirect(target)) instead of rewriting — redirects do not need the pipeline state
  3. Perform Astro rewrites from server-rendered pipeline middleware, not from adapter-bridged contexts
  4. In tests, exercise the full pipeline (app.render) rather than calling sequence() with a bare context

Example fix

// before (edge middleware)
return next('/dashboard');

// after
return context.redirect('/dashboard', 302);
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return next('/dashboard');
} catch (err) {
  if (err instanceof Error && err.message.includes('FetchState not found on APIContext')) {
    // edge-bridged context — fall back to a redirect, which needs no pipeline state
    return context.redirect('/dashboard', 302);
  }
  throw err;
}

Prevention

When it happens

Trigger: Middleware running under an adapter's own context (Vercel/Netlify edge middleware chain) calls next('/some-route'); invoking sequence() manually in tests or a custom host with a fabricated context and using the rewrite payload form.

Common situations: Moving server-side middleware that uses next(payload) rewrites to the edge; adapters that run the middleware chain outside app.render; unit tests driving sequence() with a hand-built context.

Related errors


AI-assisted analysis of withastro/astro@157c500c38 (2026-08-18). Data as JSON: /api/errors/99309e1661fa50fa. Report an issue: GitHub.