microsoft/playwright · critical · TargetClosedError

Request context disposed.

Error message

Request context disposed.

What it means

Thrown as a TargetClosedError when the request is about to be sent but the APIRequestContext has already been disposed (e.g. the owning BrowserContext was closed). It uses _closeReason if available, falling back to 'Request context disposed.'

Source

Thrown at packages/playwright-core/src/server/fetch.ts:338

    await this._updateRequestCookieHeader(progress, url, options.headers);

    const requestCookies = getHeader(options.headers, 'cookie')?.split(';').map(p => {
      const indexOfEquals = p.indexOf('=');
      const name = indexOfEquals !== -1 ? p.substring(0, indexOfEquals).trim() : p.trim();
      const value = indexOfEquals !== -1 ? p.substring(indexOfEquals + 1).trim() : '';
      return { name, value };
    }) || [];
    const requestEvent: APIRequestEvent = {
      url,
      method: options.method!,
      headers: options.headers,
      cookies: requestCookies,
      postData
    };
    this.emit(APIRequestContext.Events.Request, requestEvent);

    if (this._disposed)
      throw new TargetClosedError(this._closeReason || 'Request context disposed.');

    let destroyRequest: (() => void) | undefined;
    progress.setAllowConcurrentOrNestedRaces(true);
    const resultPromise = new Promise<SendRequestResult>((fulfill, reject) => {
      const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest)
        = (url.protocol === 'https:' ? https : http).request;
      // If we have a proxy agent already, do not override it.
      const agent = options.agent || (url.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent);
      const requestOptions = { ...options, agent };

      const startAt = monotonicTime();
      const startAtWallTime = Date.now();
      let reusedSocketAt: number | undefined;
      let dnsLookupAt: number | undefined;
      let tcpConnectionAt: number | undefined;
      let tlsHandshakeAt: number | undefined;
      let requestFinishAt: number | undefined;
      let serverIPAddress: string | undefined;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Ensure the request/context lifetime spans every call: create per-test or scope explicitly.
  2. Remove fire-and-forget requests that are not awaited before context teardown.
  3. If you need a standalone context, use request.newContext() and dispose it last.
  4. Re-check that you are not closing the browser/context inside the same test before the request resolves.

Example fix

// before: request outlives context
const context = await browser.newContext();
const req = context.request;
await context.close();
await req.get(url); // throws
// after
const req = await request.newContext();
try { await req.get(url); } finally { await req.dispose(); }
Defensive patterns

Strategy: try-catch

Validate before calling

// Track context lifecycle before issuing requests
if (context.isClosed?.() ?? false) throw new Error('context closed');
await context.request.get(url);

Type guard

// Detect a closed APIRequestContext safely
function isDisposed(e: unknown) {
  return e instanceof Error && /disposed|Target page|context has been closed/i.test(e.message);
}

Try / catch

try {
  await request.get(url);
} catch (e) {
  if (isDisposed(e)) { /* recreate context */ }
  else throw e;
}

Prevention

When it happens

Trigger: Calling request.fetch/get/post on an APIRequestContext after context.dispose(), browser.close(), or after the test/fixture has torn down the BrowserContext. Also when an async request races with afterEach cleanup.

Common situations: Using a captured request fixture across tests; awaiting a long request that outlives the context; calling request methods in a finally block after context.close() ran; shared Playwright instance reused after shutdown.

Related errors


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