From ce9f8198df67f27293b3593849f199dfaaa0b3f6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 19 Oct 2026 12:24:47 -0700 Subject: [PATCH 1/4] fix: report listener exceptions instead of TargetClosedError during binding calls Fixes: https://github.com/microsoft/playwright-java/issues/2009 --- .../java/com/microsoft/playwright/impl/BindingCall.java | 4 ++-- .../playwright/TestBrowserContextExposeFunction.java | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java index bf0683288..914896032 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java @@ -78,11 +78,11 @@ void call(BindingCallback binding) { JsonObject params = new JsonObject(); params.add("result", gson().toJsonTree(serializeArgument(result))); - sendMessage("resolve", params, NO_TIMEOUT); + sendMessageAsync("resolve", params); } catch (RuntimeException exception) { JsonObject params = new JsonObject(); params.add("error", gson().toJsonTree(serializeError(exception))); - sendMessage("reject", params, NO_TIMEOUT); + sendMessageAsync("reject", params); } } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java index b8a7f7f76..e9c0c6385 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java @@ -41,6 +41,14 @@ void exposeBindingShouldWork() { assertEquals(page, bindingSource[0].page()); assertEquals(page.mainFrame(), bindingSource[0].frame()); assertEquals(11, result); + + // https://github.com/microsoft/playwright-java/issues/2009 + page.onConsoleMessage(message -> { + throw new IllegalStateException("failure in console listener"); + }); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> page.evaluate("() => { add(1, 2); console.log('after add'); }")); + assertEquals("failure in console listener", e.getMessage()); } @Test From 9e457296e1cb3297cc0619f1c6ea481213350700 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 19 Oct 2026 12:53:29 -0700 Subject: [PATCH 2/4] fix: wait for the resolve reply and reject only when the reply fails Fixes: https://github.com/microsoft/playwright-java/issues/2009 --- .../playwright/impl/BindingCall.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java index 914896032..a9390687e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java @@ -22,6 +22,7 @@ import com.microsoft.playwright.Frame; import com.microsoft.playwright.JSHandle; import com.microsoft.playwright.Page; +import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.options.BindingCallback; import java.util.ArrayList; @@ -62,6 +63,7 @@ String name() { } void call(BindingCallback binding) { + JsonObject params = new JsonObject(); try { Frame frame = connection.getExistingObject(initializer.getAsJsonObject("frame").get("guid").getAsString()); BindingCallback.Source source = new SourceImpl(frame); @@ -75,14 +77,27 @@ void call(BindingCallback binding) { } } Object result = binding.call(source, args.toArray()); - - JsonObject params = new JsonObject(); params.add("result", gson().toJsonTree(serializeArgument(result))); - sendMessageAsync("resolve", params); } catch (RuntimeException exception) { - JsonObject params = new JsonObject(); - params.add("error", gson().toJsonTree(serializeError(exception))); - sendMessageAsync("reject", params); + reject(exception); + return; + } + WaitableResult resolved = sendMessageAsync("resolve", params); + // Exceptions thrown by event listeners while waiting for the reply are not the binding's + // failure, so they propagate to the caller as is. + while (!resolved.isDone()) { + connection.processOneMessage(); + } + try { + resolved.get(); + } catch (PlaywrightException exception) { + reject(exception); } } + + private void reject(RuntimeException exception) { + JsonObject params = new JsonObject(); + params.add("error", gson().toJsonTree(serializeError(exception))); + sendMessage("reject", params, NO_TIMEOUT); + } } From 9dbeda535e7d3471be119a53aaf16efda945c6db Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 19 Oct 2026 12:56:26 -0700 Subject: [PATCH 3/4] fix: send resolve outside of the binding's try block Fixes: https://github.com/microsoft/playwright-java/issues/2009 --- .../playwright/impl/BindingCall.java | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java index a9390687e..f4673feed 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java @@ -22,7 +22,6 @@ import com.microsoft.playwright.Frame; import com.microsoft.playwright.JSHandle; import com.microsoft.playwright.Page; -import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.options.BindingCallback; import java.util.ArrayList; @@ -79,25 +78,10 @@ void call(BindingCallback binding) { Object result = binding.call(source, args.toArray()); params.add("result", gson().toJsonTree(serializeArgument(result))); } catch (RuntimeException exception) { - reject(exception); + params.add("error", gson().toJsonTree(serializeError(exception))); + sendMessage("reject", params, NO_TIMEOUT); return; } - WaitableResult resolved = sendMessageAsync("resolve", params); - // Exceptions thrown by event listeners while waiting for the reply are not the binding's - // failure, so they propagate to the caller as is. - while (!resolved.isDone()) { - connection.processOneMessage(); - } - try { - resolved.get(); - } catch (PlaywrightException exception) { - reject(exception); - } - } - - private void reject(RuntimeException exception) { - JsonObject params = new JsonObject(); - params.add("error", gson().toJsonTree(serializeError(exception))); - sendMessage("reject", params, NO_TIMEOUT); + sendMessage("resolve", params, NO_TIMEOUT); } } From 7a63d6b8d9dd9cf2534e4e7d9704cd8829e49f45 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 19 Oct 2026 13:02:33 -0700 Subject: [PATCH 4/4] chore: send a single reply after invoking the binding Fixes: https://github.com/microsoft/playwright-java/issues/2009 --- .../playwright/impl/BindingCall.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java index f4673feed..b000a5085 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BindingCall.java @@ -62,26 +62,30 @@ String name() { } void call(BindingCallback binding) { + String method; JsonObject params = new JsonObject(); try { - Frame frame = connection.getExistingObject(initializer.getAsJsonObject("frame").get("guid").getAsString()); - BindingCallback.Source source = new SourceImpl(frame); - List args = new ArrayList<>(); - if (initializer.has("handle")) { - JSHandle handle = connection.getExistingObject(initializer.getAsJsonObject("handle").get("guid").getAsString()); - args.add(handle); - } else { - for (JsonElement arg : initializer.getAsJsonArray("args")) { - args.add(deserialize(gson().fromJson(arg, SerializedValue.class))); - } - } - Object result = binding.call(source, args.toArray()); - params.add("result", gson().toJsonTree(serializeArgument(result))); + params.add("result", gson().toJsonTree(serializeArgument(invoke(binding)))); + method = "resolve"; } catch (RuntimeException exception) { params.add("error", gson().toJsonTree(serializeError(exception))); - sendMessage("reject", params, NO_TIMEOUT); - return; + method = "reject"; + } + sendMessage(method, params, NO_TIMEOUT); + } + + private Object invoke(BindingCallback binding) { + Frame frame = connection.getExistingObject(initializer.getAsJsonObject("frame").get("guid").getAsString()); + BindingCallback.Source source = new SourceImpl(frame); + List args = new ArrayList<>(); + if (initializer.has("handle")) { + JSHandle handle = connection.getExistingObject(initializer.getAsJsonObject("handle").get("guid").getAsString()); + args.add(handle); + } else { + for (JsonElement arg : initializer.getAsJsonArray("args")) { + args.add(deserialize(gson().fromJson(arg, SerializedValue.class))); + } } - sendMessage("resolve", params, NO_TIMEOUT); + return binding.call(source, args.toArray()); } }