microsoft/playwright · error · Error

Response body is unavailable

Error message

Response body is unavailable

What it means

Thrown by createResponseBodyCallback (crNetworkManager.ts:595) when InterceptableRequest.from(request) returns undefined — the request has been detached from its CDP session (the symbol was cleared by detachIfNeeded). The response body can no longer be fetched via Network.getResponseBody because there is no session/route binding left.

Solutions

  1. Read response.body() before fulfilling or continuing the route.
  2. Cache the body bytes in the route handler if you need them later.
  3. Detach-aware: only read the body while the page that produced the response is still open.

Example fix

// before
route.fulfill({ status: 200 });
const body = await response.body();
// after
const body = await response.body();
await route.fulfill({ status: 200, body });
Defensive patterns

Strategy: validation

Validate before calling

// Read the body BEFORE fulfilling or continuing the route.
const response = await route.fetch();
const body = await response.body();
await route.fulfill({ status: response.status(), body, headers: response.headers() });

Prevention

When it happens

Trigger: Calling response.body() after route.fulfill() or route.continue() detached the request; reading the body after the page/context that owned the request was closed; accessing the body of a request whose route was already resolved.

Common situations: A request handler that fulfills a route and then logs response.body(); page.on('response') listeners that outlive the page; retries that re-read bodies after fulfill.

Related errors


AI-assisted analysis of microsoft/playwright@f1d33b5029 (2026-08-12). Data as JSON: /api/errors/a4687de1de0bda58. Report an issue: GitHub.

Appendix: source

Thrown at packages/playwright-core/src/server/chromium/crNetworkManager.ts:600

  readonly _originalRequestRoute: RouteImpl | undefined;
  session: CRSession;

  private static from(request: network.Request): InterceptableRequest | undefined {
    return (request as any)[kInterceptableRequest];
  }

  static detachIfNeeded(request: network.Request, session: CRSession) {
    if (InterceptableRequest.from(request)?.session === session)
      (request as any)[kInterceptableRequest] = undefined;
  }

  static createResponseBodyCallback(request: network.Request): () => Promise<Buffer> {
    return async () => {
      // Lookup the request lazily, so that the response does not retain the
      // InterceptableRequest and its session after detachIfNeeded().
      const interceptable = InterceptableRequest.from(request);
      if (!interceptable)
        throw new Error('Response body is unavailable');
      const contentLength = request._existingResponse()?.headerValue('content-length');
      const expectedLength = contentLength ? +contentLength : undefined;

      const session = interceptable.session;
      const response = await session.send('Network.getResponseBody', { requestId: interceptable._requestId });
      if (response.body || !expectedLength)
        return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');

      // Make sure no network requests sent while reading the body for fulfilled requests.
      if (interceptable._originalRequestRoute?._fulfilled)
        return Buffer.from('');

      // Re-fetching the resource may produce side effects on the server, only
      // do it for GETs of static subresources and prefetch requests.
      if (request.method() !== 'GET')
        return Buffer.from('');
      if (!kRefetchSafeResourceTypes.has(request.resourceType())) {
        const rawHeaders = await request.internalRawRequestHeaders();

View on GitHub (pinned to f1d33b5029)