withastro/astro · error · AstroError

ResponseSentError

ResponseSentError

Error message

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

What it means

In streaming (ReadableStream) rendering, once the first chunk is enqueued the status/headers are committed to the client and can no longer change. If a Response object (typically a redirect produced by Astro.redirect) is rendered eagerly and then reaches the stream's write path after output has flushed, Astro throws ResponseSentError instead of silently dropping the redirect.

Solutions

  1. Move redirects into page frontmatter before any markup is produced, or into middleware (onRequest / next()) where nothing has flushed
  2. Do auth/session checks in middleware so redirects happen before render starts
  3. Ensure components/layouts never return a Response; only pages may return one
  4. If data may arrive late, redirect from middleware or the page's frontmatter await, not from a child component

Example fix

// before: redirect inside a layout
---
if (!user) return Astro.redirect('/login');
---

// after: redirect in middleware
export const onRequest = async (context, next) => {
  const user = await getUser(context.request);
  if (!user && context.url.pathname.startsWith('/dashboard')) {
    return context.redirect('/login');
  }
  return next();
};
Defensive patterns

Strategy: validation

Validate before calling

// middleware: decide before anything renders
export const onRequest = async (context, next) => {
  const user = await getUser(context.request);
  if (!user && context.url.pathname.startsWith('/app')) {
    return context.redirect('/login');
  }
  return next();
};

Prevention

When it happens

Trigger: Returning Astro.redirect(...) (or any Response) from a layout, a slot's content, or a nested component rendered after the page's first chunk already flushed; awaiting slow data in frontmatter and returning a redirect only after streaming has begun.

Common situations: Auth checks placed in a layout instead of middleware or page frontmatter; redirects raised from deep inside slot content or try/catch handlers; mixing streamed rendering with control-flow Responses.

Related errors


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

Appendix: source

Thrown at packages/astro/src/runtime/server/render/astro/render.ts:96

	return new ReadableStream({
		start(controller) {
			const destination: RenderDestination = {
				write(chunk) {
					// Automatic doctype insertion for pages
					if (isPage && !renderedFirstPageChunk) {
						renderedFirstPageChunk = true;
						if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) {
							const doctype = result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n';
							controller.enqueue(encoder.encode(doctype));
						}
					}

					// `chunk` might be a Response that contains a redirect,
					// that was rendered eagerly and therefore bypassed the early check
					// whether headers can still be modified. In that case, throw an error
					if (chunk instanceof Response) {
						throw new AstroError({
							...AstroErrorData.ResponseSentError,
						});
					}

					const bytes = chunkToByteArray(result, chunk);
					controller.enqueue(bytes);
				},
			};

			(async () => {
				try {
					await renderStreaming(templateResult, result, destination);
					controller.close();
				} catch (e) {
					// We don't have a lot of information downstream, and upstream we can't catch the error properly
					// So let's add the location here
					if (AstroError.is(e) && !e.loc) {
						e.setLocation({

View on GitHub (pinned to 52e6c34790)