From 194046d013e636f578cf287ca87f78640419c57b Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sat, 14 Feb 2026 02:45:57 -0800 Subject: [PATCH 1/4] fix: clamp setTimeout value to prevent negative timeout crash When a Node.js process has been running for more than ~24.8 days, monotonicTime() exceeds 2^31-1 ms. This causes the computed timeout in raceAgainstDeadline() to become negative, which Node.js coerces to 1ms, resulting in an immediate spurious timeout. Clamp the timeout to [0, 2^31-1] to prevent both negative values and values exceeding setTimeout's maximum safe delay. Fixes #39166 --- .../playwright-core/src/utils/isomorphic/timeoutRunner.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts index 9a01d0f0796cc..078cf03e11a31 100644 --- a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts +++ b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts @@ -30,7 +30,11 @@ export async function raceAgainstDeadline(cb: () => Promise, deadline: num }), new Promise<{ timedOut: true }>(resolve => { const kMaxDeadline = 2147483647; // 2^31-1 - const timeout = (deadline || kMaxDeadline) - monotonicTime(); + const rawTimeout = (deadline || kMaxDeadline) - monotonicTime(); + // Clamp to [0, kMaxDeadline] to avoid negative values (which Node.js + // coerces to 1ms) when the process uptime exceeds the deadline, and to + // stay within the valid range for setTimeout. + const timeout = Math.min(Math.max(rawTimeout, 0), kMaxDeadline); timer = setTimeout(() => resolve({ timedOut: true }), timeout); }), ]).finally(() => { From cee5af972b4eef4d5595425d6733a4408e13085e Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sun, 15 Feb 2026 16:11:29 -0800 Subject: [PATCH 2/4] fix: skip timer entirely when no deadline is set Instead of falling back to kMaxDeadline and clamping, just return early from the promise executor when deadline is 0. This avoids the negative timeout issue altogether by not creating a timer when none is needed. Co-Authored-By: Claude Opus 4.6 --- .../src/utils/isomorphic/timeoutRunner.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts index 078cf03e11a31..ea128ee74929b 100644 --- a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts +++ b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts @@ -29,13 +29,11 @@ export async function raceAgainstDeadline(cb: () => Promise, deadline: num return { result, timedOut: false }; }), new Promise<{ timedOut: true }>(resolve => { + if (!deadline) + return; const kMaxDeadline = 2147483647; // 2^31-1 - const rawTimeout = (deadline || kMaxDeadline) - monotonicTime(); - // Clamp to [0, kMaxDeadline] to avoid negative values (which Node.js - // coerces to 1ms) when the process uptime exceeds the deadline, and to - // stay within the valid range for setTimeout. - const timeout = Math.min(Math.max(rawTimeout, 0), kMaxDeadline); - timer = setTimeout(() => resolve({ timedOut: true }), timeout); + const timeout = Math.max(deadline - monotonicTime(), 0); + timer = setTimeout(() => resolve({ timedOut: true }), Math.min(timeout, kMaxDeadline)); }), ]).finally(() => { clearTimeout(timer); From 93d6e8e9095a88a3658a564336de1c378cc4b444 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Mon, 16 Feb 2026 11:12:04 -0800 Subject: [PATCH 3/4] fix: clamp deadline - monotonicTime() to zero in progress.ts and locator.ts Apply the same negative timeout prevention to other places in the codebase that compute deadline - monotonicTime() without clamping. This prevents passing negative values to setTimeout when the deadline has already passed. --- packages/playwright-core/src/client/locator.ts | 2 +- packages/playwright-core/src/server/progress.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 86650c00347ed..bb33f414dd660 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -82,7 +82,7 @@ export class Locator implements api.Locator { if (!handle) throw new Error(`Could not resolve ${this._selector} to DOM Element`); try { - return await task(handle, deadline ? deadline - monotonicTime() : 0); + return await task(handle, deadline ? Math.max(deadline - monotonicTime(), 0) : 0); } finally { await handle.dispose(); } diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index b94ef285fdfb1..b28dcdfb60098 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -107,7 +107,7 @@ export class ProgressController { this._forceAbortPromise.reject(timeoutError); this._controller.abort(timeoutError); } - }, deadline - monotonicTime()); + }, Math.max(deadline - monotonicTime(), 0)); } try { From d9cf0920471f78f39d4e84944383d4c8d38e35cb Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Mon, 16 Feb 2026 15:56:41 -0800 Subject: [PATCH 4/4] fix: skip timer entirely when deadline has already passed Instead of clamping to Math.max(..., 0) which still schedules a 0ms timer, check if the deadline has passed and either resolve/reject immediately or skip the setTimeout altogether. Reverted the unnecessary Math.max change in locator.ts where it doesn't apply. --- .../playwright-core/src/client/locator.ts | 2 +- .../playwright-core/src/server/progress.ts | 20 ++++++++++++++----- .../src/utils/isomorphic/timeoutRunner.ts | 7 ++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index bb33f414dd660..86650c00347ed 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -82,7 +82,7 @@ export class Locator implements api.Locator { if (!handle) throw new Error(`Could not resolve ${this._selector} to DOM Element`); try { - return await task(handle, deadline ? Math.max(deadline - monotonicTime(), 0) : 0); + return await task(handle, deadline ? deadline - monotonicTime() : 0); } finally { await handle.dispose(); } diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index b28dcdfb60098..b9713428e58da 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -98,16 +98,26 @@ export class ProgressController { if (deadline) { const timeoutError = new TimeoutError(`Timeout ${timeout}ms exceeded.`); - timer = setTimeout(() => { - // TODO: migrate this to "progress.disableTimeout()". - if (this.metadata.pauseStartTime && !this.metadata.pauseEndTime) - return; + const remaining = deadline - monotonicTime(); + if (remaining <= 0) { + // Deadline already passed, fire immediately without scheduling a timer. if (this._state === 'running') { this._state = { error: timeoutError }; this._forceAbortPromise.reject(timeoutError); this._controller.abort(timeoutError); } - }, Math.max(deadline - monotonicTime(), 0)); + } else { + timer = setTimeout(() => { + // TODO: migrate this to "progress.disableTimeout()". + if (this.metadata.pauseStartTime && !this.metadata.pauseEndTime) + return; + if (this._state === 'running') { + this._state = { error: timeoutError }; + this._forceAbortPromise.reject(timeoutError); + this._controller.abort(timeoutError); + } + }, remaining); + } } try { diff --git a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts index ea128ee74929b..2aec3834e43ed 100644 --- a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts +++ b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts @@ -32,7 +32,12 @@ export async function raceAgainstDeadline(cb: () => Promise, deadline: num if (!deadline) return; const kMaxDeadline = 2147483647; // 2^31-1 - const timeout = Math.max(deadline - monotonicTime(), 0); + const timeout = deadline - monotonicTime(); + if (timeout <= 0) { + // Deadline already passed, resolve immediately without scheduling a timer. + resolve({ timedOut: true }); + return; + } timer = setTimeout(() => resolve({ timedOut: true }), Math.min(timeout, kMaxDeadline)); }), ]).finally(() => {