From 12eba881887c604a86532f2f174a2dde8d3385c1 Mon Sep 17 00:00:00 2001 From: Joe Bandenburg Date: Fri, 19 Apr 2024 16:41:19 -0400 Subject: [PATCH] [cdp][java] Allow filters to recover from failed requests in NetworkInterceptor This change introduces a new exception, which is thrown through the user's filter chain when the browser fails to get a response for a request and a NetworkInterceptor is in use. This gives the filter an opportunity to catch the exception and return a custom HTTP response. Related to #13774 --- .../devtools/RequestFailedException.java | 13 +++++++++++++ .../selenium/devtools/idealized/Network.java | 18 ++++++++++++++++-- .../devtools/NetworkInterceptorTest.java | 19 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 java/src/org/openqa/selenium/devtools/RequestFailedException.java diff --git a/java/src/org/openqa/selenium/devtools/RequestFailedException.java b/java/src/org/openqa/selenium/devtools/RequestFailedException.java new file mode 100644 index 00000000000000..74add97500ee73 --- /dev/null +++ b/java/src/org/openqa/selenium/devtools/RequestFailedException.java @@ -0,0 +1,13 @@ +package org.openqa.selenium.devtools; + +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.remote.http.Filter; +import org.openqa.selenium.remote.http.HttpHandler; + +/** + * This exception is thrown by the final {@link HttpHandler} in a {@link Filter} chain when the + * browser fails to send a HTTP request. It can be caught in a {@link Filter} to handle the error + * by, for example, returning a custom HTTP response. + */ +public class RequestFailedException extends WebDriverException { +} diff --git a/java/src/org/openqa/selenium/devtools/idealized/Network.java b/java/src/org/openqa/selenium/devtools/idealized/Network.java index 2c09e877a79736..ec36b5d40d5d05 100644 --- a/java/src/org/openqa/selenium/devtools/idealized/Network.java +++ b/java/src/org/openqa/selenium/devtools/idealized/Network.java @@ -45,6 +45,7 @@ import org.openqa.selenium.devtools.DevToolsException; import org.openqa.selenium.devtools.Event; import org.openqa.selenium.devtools.NetworkInterceptor; +import org.openqa.selenium.devtools.RequestFailedException; import org.openqa.selenium.internal.Either; import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.http.Contents; @@ -202,8 +203,12 @@ public void prepareToInterceptTraffic() { String id = getRequestId(pausedRequest); if (hasErrorResponse(pausedRequest)) { - pendingResponses.remove(id); - devTools.send(continueWithoutModification(pausedRequest)); + CompletableFuture future = pendingResponses.remove(id); + if (future == null) { + devTools.send(continueWithoutModification(pausedRequest)); + } else { + future.completeExceptionally(new RequestFailedException()); + } return; } @@ -244,6 +249,11 @@ public void prepareToInterceptTraffic() { pendingResponses.remove(id); return STOP_PROCESSING; } catch (ExecutionException e) { + if (e.getCause() instanceof RequestFailedException) { + // Throwing here will give the user's filter a chance to intercept + // the failure and handle it. + throw (RequestFailedException) e.getCause(); + } if (fetchEnabled.get()) { LOG.log(WARNING, e, () -> "Unable to process request"); } @@ -261,6 +271,10 @@ public void prepareToInterceptTraffic() { } devTools.send(fulfillRequest(pausedRequest, forBrowser)); + } catch (RequestFailedException e) { + // If the exception reaches here, we know the user's filter has not handled it and the + // browser should continue its normal error handling. + devTools.send(continueWithoutModification(pausedRequest)); } catch (TimeoutException e) { if (fetchEnabled.get()) { throw e; diff --git a/java/test/org/openqa/selenium/devtools/NetworkInterceptorTest.java b/java/test/org/openqa/selenium/devtools/NetworkInterceptorTest.java index f916a1772c115b..361affcd732f16 100644 --- a/java/test/org/openqa/selenium/devtools/NetworkInterceptorTest.java +++ b/java/test/org/openqa/selenium/devtools/NetworkInterceptorTest.java @@ -254,11 +254,28 @@ void shouldHandleRedirects() { @Test @NoDriverBeforeTest - void shouldProceedAsNormalIfRequestResultInAnKnownError() { + void shouldProceedAsNormalIfRequestResultInAnKnownErrorAndExceptionNotCaughtByFilter() { Filter filter = next -> next; try (NetworkInterceptor ignored = new NetworkInterceptor(driver, filter)) { assertThatExceptionOfType(WebDriverException.class) .isThrownBy(() -> driver.get("http://localhost:" + PortProber.findFreePort())); } } + + @Test + @NoDriverBeforeTest + void shouldPassResponseBackToBrowserIfRequestResultsInAnKnownErrorAndExceptionCaughtByFilter() { + Filter filter = next -> req -> { + try { + return next.execute(req); + } catch (RequestFailedException e) { + return new HttpResponse().setStatus(200).setContent(Contents.utf8String("Hello, World!")); + } + }; + try (NetworkInterceptor ignored = new NetworkInterceptor(driver, filter)) { + driver.get("http://localhost:" + PortProber.findFreePort()); + String body = driver.findElement(By.tagName("body")).getText(); + assertThat(body).contains("Hello, World!"); + } + } }