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
- Do not call fetch on a disposed APIRequestContext — create a fresh one via request.newContext() or the fixture.
- Ensure ordering: perform all requests before context.dispose()/browser.close().
- If you need post-close requests, allocate a new APIRequestContext tied to a still-live browser.
- 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
- Perform all requests before request.dispose()/context.close().
- Don't share an APIRequestContext past its owning context's lifetime.
- In tests, rely on the request fixture rather than managing lifecycle by hand.
- Track disposed state explicitly if you long-live a context.
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
- Target page, context or browser has been closed
- Request context disposed.
- closed
- Target page, context or browser has been closed
- The object has been collected to prevent unbounded heap grow
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/17bd2486c73f8fba.
Report an issue: GitHub.