withastro/astro · error · AstroError

ResponseSentError

ResponseSentError

Error message

The response has already been sent to the browser and cannot be altered.

What it means

The page-level Astro.redirect() checks a private responseSent marker on the request before building the redirect Response. Once streaming to the browser has started, the status and headers can no longer change, so redirect() throws AstroErrorData.ResponseSentError instead of silently doing nothing. This protects against half-sent pages that would otherwise redirect inconsistently.

Solutions

  1. Do redirect decisions in the frontmatter before any markup is returned, or better, in middleware (context.redirect) before rendering starts
  2. Move slow auth/session lookups ahead of the first flush point so the redirect verdict is known before streaming
  3. If the redirect decision genuinely arrives after flush, emit an HTML meta-refresh or a small client-side redirect script instead of calling Astro.redirect()

Example fix

// before — decides after streaming began
const user = await slowAuthCheck();
if (!user) return Astro.redirect('/login');

// after — guard before rendering starts (middleware)
export const onRequest = async (context, next) => {
  if (!context.cookies.get('session')) return context.redirect('/login', 302);
  return next();
};
Defensive patterns

Strategy: try-catch

Try / catch

import { AstroError } from 'astro/errors';
try {
  return Astro.redirect('/login');
} catch (err) {
  if (err instanceof AstroError && err.code === 'ResponseSentError') {
    // headers can no longer change — fall back to an HTML-level redirect
    return new Response(
      '<!doctype html><meta http-equiv="refresh" content="0;url=/login">',
      { headers: { 'content-type': 'text/html' } }
    );
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling Astro.redirect() after the first chunk of the response has been flushed: inside a component or slot rendered mid-stream, after an awaited slow data fetch while the shell is already streaming, or calling redirect a second time after the response began.

Common situations: Auth or permission checks that run after an expensive await inside a streamed page; layouts or islands that redirect conditionally but render after content has flushed; upgrading from buffered rendering to streaming and finding late redirects now fail.

Related errors


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

Appendix: source

Thrown at packages/astro/src/core/fetch/fetch-state.ts:573

			},
		});

		return Astro as AstroGlobal;
	}

	/**
	 * Creates the Astro page-level partial (prototype for Astro global).
	 */
	createAstroPagePartial(
		result: SSRResult,
		apiContext: ActionAPIContext,
	): Omit<AstroGlobal, 'props' | 'self' | 'slots'> {
		const state = this;
		const { cookies, locals, params, logger, url } = this;
		const { response } = result;
		const redirect = (path: string, status = 302) => {
			if ((state.request as any)[responseSentSymbol]) {
				throw new AstroError({
					...AstroErrorData.ResponseSentError,
				});
			}
			return new Response(null, { status, headers: { Location: path } });
		};

		const rewrite = async (reroutePayload: RewritePayload) => {
			return await state.rewrite(reroutePayload);
		};

		const callAction = createCallAction(apiContext);

		const partial: Record<string, any> = {
			generator: ASTRO_GENERATOR,
			routePattern: this.routeData!.route,
			isPrerendered: this.routeData!.prerender,
			cookies,
			get clientAddress() {

View on GitHub (pinned to e294953aa8)