microsoft/playwright · error · TargetClosedError

${this._closeReason}

Error message

${this._closeReason}

What it means

Thrown by APIRequestContext._innerFetch (wrapped in _wrapApiCall) when this._closeReason is truthy at call entry. _closeReason is set by dispose({ reason }), so any request issued after the APIRequestContext was disposed fails with a TargetClosedError whose message is the user-supplied (or default) reason. The browser/page/context objects carry the same pattern.

Source

Thrown at packages/playwright-core/src/client/fetch.ts:182

  }

  async put(url: string, options?: RequestWithBodyOptions): Promise<APIResponse> {
    return await this.fetch(url, {
      ...options,
      method: 'PUT',
    });
  }

  async fetch(urlOrRequest: string | api.Request, options: FetchOptions = {}): Promise<APIResponse> {
    const url = isString(urlOrRequest) ? urlOrRequest : undefined;
    const request = isString(urlOrRequest) ? undefined : urlOrRequest;
    return await this._innerFetch({ url, request, ...options });
  }

  async _innerFetch(options: FetchOptions & { url?: string, request?: api.Request } = {}): Promise<APIResponse> {
    return await this._wrapApiCall(async () => {
      if (this._closeReason)
        throw new TargetClosedError(this._closeReason);
      assert(options.request || typeof options.url === 'string', 'First argument must be either URL string or Request');
      assert((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
      assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
      assert(options.maxRetries === undefined || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`);
      const url = options.url !== undefined ? options.url : options.request!.url();
      const method = options.method || options.request?.method();
      let encodedParams = undefined;
      if (typeof options.params === 'string')
        encodedParams = options.params;
      else if (options.params instanceof URLSearchParams)
        encodedParams = options.params.toString();
      // Cannot call allHeaders() here as the request may be paused inside route handler.
      const headersObj = options.headers || options.request?.headers();
      const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
      let jsonData: any;
      let formData: channels.NameValue[] | undefined;
      let multipartData: channels.FormField[] | undefined;
      let postDataBuffer: Buffer | undefined;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Do not call fetch on a disposed APIRequestContext — create a fresh one via request.newContext() or the fixture.
  2. Ensure ordering: perform all requests before context.dispose()/browser.close().
  3. If you need post-close requests, allocate a new APIRequestContext tied to a still-live browser.
  4. Check request._closeReason (or wrap calls) before issuing requests in shared/long-lived code.

Example fix

// before
const request = await playwright.request.newContext();
await request.dispose();
await request.get('https://example.com/api'); // TargetClosedError

// after
const request = await playwright.request.newContext();
await request.get('https://example.com/api');
await request.dispose();
Defensive patterns

Strategy: type-guard

Validate before calling

// Guard before issuing requests on a shared/long-lived APIRequestContext.
function isRequestContextClosed(ctx) {
  // No public flag; emulate by tracking dispose() yourself.
  return ctx.__disposed === true;
}
async function safeFetch(ctx, url, init) {
  if (isRequestContextClosed(ctx)) throw new Error('APIRequestContext disposed; create a new one.');
  return ctx.fetch(url, init);
}
// Wrap dispose to mark:
const origDispose = ctx.dispose.bind(ctx);
ctx.dispose = async (o) => { ctx.__disposed = true; return origDispose(o); };

Type guard

import type { APIRequestContext } from 'playwright-core';
// Track lifecycle at the type level via a branded wrapper.
type Live<T> = T & { __alive?: true };
type Disposed = { __alive?: false };
function assertLive(ctx: APIRequestContext & Partial<{ __alive?: boolean }>): asserts ctx is Live<APIRequestContext> {
  if (ctx.__alive === false) throw new Error('APIRequestContext is disposed');
}

Try / catch

try {
  await request.get(url);
} catch (e) {
  if (e?.name === 'TargetClosedError' || /closed|disposed/i.test(String(e?.message))) {
    request = await playwright.request.newContext();
    return request.get(url);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling request.get/post/... (or apiRequestContext.fetch) after request.dispose([...]) has run, after the owning BrowserContext was closed, or after the test fixture tore down the APIRequestContext. The check runs before any URL/argument validation, so the close reason is the first thing surfaced.

Common situations: Using a shared APIRequestContext across tests and disposing it early; issuing requests in afterAll/teardown after context cleanup; leak of a context reference past page.close(); reusing a context whose underlying browser disconnected.

Related errors


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