From f780c7de7256f4c659c7f82ccd33cb024e952aa2 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 14 Oct 2026 14:40:48 -0700 Subject: [PATCH 1/4] fix: time out on the client when the driver stops responding Fixes: https://github.com/microsoft/playwright-java/issues/1990 Fixes: https://github.com/microsoft/playwright-java/issues/1810 --- .../playwright/impl/BrowserContextImpl.java | 9 ++- .../playwright/impl/CDPSessionImpl.java | 4 +- .../microsoft/playwright/impl/Connection.java | 25 ++++++- .../TestBrowserContextCDPSession.java | 23 ++++++ .../playwright/TestUnresponsiveDriver.java | 70 +++++++++++++++++++ 5 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index 4e018748b..ae5213e98 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -357,7 +357,9 @@ public CDPSession newCDPSession(Page page) { JsonObject params = new JsonObject(); params.add("page", ((PageImpl) page).toProtocolRef()); JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject(); - return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); + CDPSessionImpl session = connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); + session.timeoutSettings = ((PageImpl) page).timeoutSettings; + return session; } @Override @@ -365,7 +367,10 @@ public CDPSession newCDPSession(Frame frame) { JsonObject params = new JsonObject(); params.add("frame", ((FrameImpl) frame).toProtocolRef()); JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject(); - return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); + CDPSessionImpl session = connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); + PageImpl page = ((FrameImpl) frame).page(); + session.timeoutSettings = page != null ? page.timeoutSettings : timeoutSettings; + return session; } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java index 24afe0726..25564c7d3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java @@ -26,6 +26,8 @@ public class CDPSessionImpl extends ChannelOwner implements CDPSession { private final ListenerCollection listeners = new ListenerCollection<>(new HashMap<>(), this); private final ListenerCollection typedListeners = new ListenerCollection<>(new HashMap<>(), this); + // Set for page and frame sessions, browser sessions have no timeout. + TimeoutSettings timeoutSettings; enum EventType { CLOSE, @@ -71,7 +73,7 @@ public JsonObject send(String method, JsonObject params) { args.add("params", params); } args.addProperty("method", method); - JsonElement response = connection.sendMessage(guid, "send", args); + JsonElement response = sendMessage("send", args, timeoutSettings == null ? NO_TIMEOUT : timeoutSettings.timeout(null)); if (response == null) return null; else return response.getAsJsonObject().get("result").getAsJsonObject(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index 83a7e17bd..c26ba3cc8 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import static com.microsoft.playwright.impl.Serialization.gson; import static java.lang.System.currentTimeMillis; @@ -69,6 +70,9 @@ public class Connection { private final Map> callbacks = new HashMap<>(); private String title; private boolean titleReported = false; + // How long past a call's timeout to wait for the driver's reply. The driver may spend up to 3s + // in tracing instrumentation both before and after the call itself. + private static final long DRIVER_RESPONSE_GRACE_MS = 10_000; private static final boolean isLogging; static { String debug = System.getenv("DEBUG"); @@ -145,7 +149,20 @@ public JsonElement sendMessage(String guid, String method, JsonObject params) { } public JsonElement sendMessage(String guid, String method, JsonObject params, Double timeout) { - return root.runUntil(() -> {}, internalSendMessage(guid, method, params, timeout, true, true)); + WaitableResult result = internalSendMessage(guid, method, params, timeout, true, true); + if (timeout == null || timeout == 0) { + return root.runUntil(() -> {}, result); + } + // The timeout is enforced by the driver, but if the driver or the browser hangs no reply ever + // comes. Give up once the driver is well past the timeout and has stopped sending messages. + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout.longValue() + DRIVER_RESPONSE_GRACE_MS); + while (!result.isDone()) { + if (!processOneMessage() && System.nanoTime() - deadline > 0) { + result.completeExceptionally(new TimeoutError("Timeout " + timeout.longValue() + "ms exceeded while waiting for the driver to respond to " + + method + ". The driver or the browser process is not responding.")); + } + } + return result.get(); } public WaitableResult sendMessageAsync(String guid, String method, JsonObject params) { @@ -232,14 +249,16 @@ void unregisterObject(String guid) { objects.remove(guid); } - void processOneMessage() { + // Returns false if no message arrived within the poll interval. + boolean processOneMessage() { JsonObject message = transport.poll(Duration.ofMillis(10)); if (message == null) { - return; + return false; } Gson gson = gson(); Message messageObj = gson.fromJson(message, Message.class); dispatch(messageObj); + return true; } private static String formatCallLog(JsonArray log) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java index bf0ef7d5a..d35ba2b56 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java @@ -44,6 +44,29 @@ void shouldWork() { assertEquals("bar", foo); } + @Test + void shouldRespectDefaultTimeout() { + page.context().setDefaultTimeout(500); + CDPSession cdpSession = page.context().newCDPSession(page); + JsonObject params = new JsonObject(); + params.addProperty("expression", "new Promise(() => {})"); + params.addProperty("awaitPromise", true); + TimeoutError e = assertThrows(TimeoutError.class, () -> cdpSession.send("Runtime.evaluate", params)); + assertTrue(e.getMessage().contains("Timeout 500ms exceeded"), e.getMessage()); + } + + @Test + void shouldNotTimeOutWhenDefaultTimeoutIsZero() { + page.setDefaultTimeout(0); + CDPSession cdpSession = page.context().newCDPSession(page); + JsonObject params = new JsonObject(); + params.addProperty("expression", "new Promise(f => setTimeout(() => f(42), 100))"); + params.addProperty("awaitPromise", true); + params.addProperty("returnByValue", true); + JsonObject result = cdpSession.send("Runtime.evaluate", params); + assertEquals(42, result.getAsJsonObject("result").get("value").getAsInt()); + } + @Test void shouldSendEvents() { CDPSession cdpSession = page.context().newCDPSession(page); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java b/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java new file mode 100644 index 000000000..07fd4d70f --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class TestUnresponsiveDriver { + @Test + @DisabledOnOs(OS.WINDOWS) + void shouldThrowTimeoutErrorWhenDriverStopsResponding() throws Exception { + try (Playwright playwright = Playwright.create()) { + Browser browser = Utils.getBrowserTypeFromEnv(playwright).launch(TestBase.createLaunchOptions()); + Page page = browser.newPage(); + long pid = driverPid(playwright); + signal("STOP", pid); + try { + long start = System.nanoTime(); + TimeoutError e = assertThrows(TimeoutError.class, + () -> page.waitForFunction("() => false", null, new Page.WaitForFunctionOptions().setTimeout(100))); + long elapsedMs = (System.nanoTime() - start) / 1_000_000; + assertTrue(e.getMessage().contains("Timeout 100ms exceeded while waiting for the driver to respond to waitForFunction"), e.getMessage()); + assertTrue(elapsedMs < 20_000, "elapsed: " + elapsedMs); + } finally { + signal("CONT", pid); + } + // The driver's late reply to the abandoned call must not break the connection. + assertEquals(2, page.evaluate("1 + 1")); + } + } + + private static long driverPid(Playwright playwright) throws Exception { + Field field = playwright.getClass().getDeclaredField("driverProcess"); + field.setAccessible(true); + Process process = (Process) field.get(playwright); + long pid = -1; + try { + // Process.pid() is only available since Java 9. + pid = (long) Process.class.getMethod("pid").invoke(process); + } catch (NoSuchMethodException e) { + assumeTrue(false, "Process.pid() is not available"); + } + return pid; + } + + private static void signal(String signal, long pid) throws Exception { + Process kill = new ProcessBuilder("kill", "-" + signal, Long.toString(pid)).inheritIO().start(); + assertEquals(0, kill.waitFor()); + } +} From 76ffd9662903eb7b3b84de67dccf003339831a1a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 14 Oct 2026 15:43:42 -0700 Subject: [PATCH 2/4] chore: drop CDPSession.send timeout, it will come from upstream --- .../playwright/impl/BrowserContextImpl.java | 9 ++------ .../playwright/impl/CDPSessionImpl.java | 4 +--- .../TestBrowserContextCDPSession.java | 23 ------------------- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index ae5213e98..4e018748b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -357,9 +357,7 @@ public CDPSession newCDPSession(Page page) { JsonObject params = new JsonObject(); params.add("page", ((PageImpl) page).toProtocolRef()); JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject(); - CDPSessionImpl session = connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); - session.timeoutSettings = ((PageImpl) page).timeoutSettings; - return session; + return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); } @Override @@ -367,10 +365,7 @@ public CDPSession newCDPSession(Frame frame) { JsonObject params = new JsonObject(); params.add("frame", ((FrameImpl) frame).toProtocolRef()); JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject(); - CDPSessionImpl session = connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); - PageImpl page = ((FrameImpl) frame).page(); - session.timeoutSettings = page != null ? page.timeoutSettings : timeoutSettings; - return session; + return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java index 25564c7d3..24afe0726 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java @@ -26,8 +26,6 @@ public class CDPSessionImpl extends ChannelOwner implements CDPSession { private final ListenerCollection listeners = new ListenerCollection<>(new HashMap<>(), this); private final ListenerCollection typedListeners = new ListenerCollection<>(new HashMap<>(), this); - // Set for page and frame sessions, browser sessions have no timeout. - TimeoutSettings timeoutSettings; enum EventType { CLOSE, @@ -73,7 +71,7 @@ public JsonObject send(String method, JsonObject params) { args.add("params", params); } args.addProperty("method", method); - JsonElement response = sendMessage("send", args, timeoutSettings == null ? NO_TIMEOUT : timeoutSettings.timeout(null)); + JsonElement response = connection.sendMessage(guid, "send", args); if (response == null) return null; else return response.getAsJsonObject().get("result").getAsJsonObject(); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java index d35ba2b56..bf0ef7d5a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java @@ -44,29 +44,6 @@ void shouldWork() { assertEquals("bar", foo); } - @Test - void shouldRespectDefaultTimeout() { - page.context().setDefaultTimeout(500); - CDPSession cdpSession = page.context().newCDPSession(page); - JsonObject params = new JsonObject(); - params.addProperty("expression", "new Promise(() => {})"); - params.addProperty("awaitPromise", true); - TimeoutError e = assertThrows(TimeoutError.class, () -> cdpSession.send("Runtime.evaluate", params)); - assertTrue(e.getMessage().contains("Timeout 500ms exceeded"), e.getMessage()); - } - - @Test - void shouldNotTimeOutWhenDefaultTimeoutIsZero() { - page.setDefaultTimeout(0); - CDPSession cdpSession = page.context().newCDPSession(page); - JsonObject params = new JsonObject(); - params.addProperty("expression", "new Promise(f => setTimeout(() => f(42), 100))"); - params.addProperty("awaitPromise", true); - params.addProperty("returnByValue", true); - JsonObject result = cdpSession.send("Runtime.evaluate", params); - assertEquals(42, result.getAsJsonObject("result").get("value").getAsInt()); - } - @Test void shouldSendEvents() { CDPSession cdpSession = page.context().newCDPSession(page); From 72573e5ccd45700ce5399e6cdd355fe49b6eba93 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 14 Oct 2026 15:48:35 -0700 Subject: [PATCH 3/4] chore: shorten comments --- .../java/com/microsoft/playwright/impl/Connection.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index c26ba3cc8..b7c0b0144 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -70,8 +70,6 @@ public class Connection { private final Map> callbacks = new HashMap<>(); private String title; private boolean titleReported = false; - // How long past a call's timeout to wait for the driver's reply. The driver may spend up to 3s - // in tracing instrumentation both before and after the call itself. private static final long DRIVER_RESPONSE_GRACE_MS = 10_000; private static final boolean isLogging; static { @@ -153,13 +151,12 @@ public JsonElement sendMessage(String guid, String method, JsonObject params, Do if (timeout == null || timeout == 0) { return root.runUntil(() -> {}, result); } - // The timeout is enforced by the driver, but if the driver or the browser hangs no reply ever - // comes. Give up once the driver is well past the timeout and has stopped sending messages. + // The driver enforces the timeout, but the driver itself may hang. long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout.longValue() + DRIVER_RESPONSE_GRACE_MS); while (!result.isDone()) { if (!processOneMessage() && System.nanoTime() - deadline > 0) { result.completeExceptionally(new TimeoutError("Timeout " + timeout.longValue() + "ms exceeded while waiting for the driver to respond to " + - method + ". The driver or the browser process is not responding.")); + method + ". The driver is not responding.")); } } return result.get(); @@ -249,7 +246,6 @@ void unregisterObject(String guid) { objects.remove(guid); } - // Returns false if no message arrived within the poll interval. boolean processOneMessage() { JsonObject message = transport.poll(Duration.ofMillis(10)); if (message == null) { From 947afffa7936cfea442456d1b8521b956fdcdde7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 14 Oct 2026 15:50:29 -0700 Subject: [PATCH 4/4] test: use @EnabledForJreRange instead of an assumption --- .../playwright/TestUnresponsiveDriver.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java b/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java index 07fd4d70f..e211a648c 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java @@ -18,16 +18,18 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.condition.OS; import java.lang.reflect.Field; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.assumeTrue; public class TestUnresponsiveDriver { @Test @DisabledOnOs(OS.WINDOWS) + @EnabledForJreRange(min = JRE.JAVA_9) void shouldThrowTimeoutErrorWhenDriverStopsResponding() throws Exception { try (Playwright playwright = Playwright.create()) { Browser browser = Utils.getBrowserTypeFromEnv(playwright).launch(TestBase.createLaunchOptions()); @@ -52,15 +54,8 @@ void shouldThrowTimeoutErrorWhenDriverStopsResponding() throws Exception { private static long driverPid(Playwright playwright) throws Exception { Field field = playwright.getClass().getDeclaredField("driverProcess"); field.setAccessible(true); - Process process = (Process) field.get(playwright); - long pid = -1; - try { - // Process.pid() is only available since Java 9. - pid = (long) Process.class.getMethod("pid").invoke(process); - } catch (NoSuchMethodException e) { - assumeTrue(false, "Process.pid() is not available"); - } - return pid; + // Called via reflection as the tests are compiled with JDK 8 on CI. + return (long) Process.class.getMethod("pid").invoke(field.get(playwright)); } private static void signal(String signal, long pid) throws Exception {