From 5cd4bd2477e68ea35874913a1a7ef2a00e3bf382 Mon Sep 17 00:00:00 2001 From: Joe Bandenburg Date: Tue, 7 May 2024 01:24:59 -0400 Subject: [PATCH] [cdp][java] Allow filters to recover from failed requests in NetworkInterceptor (#13847) Co-authored-by: Puja Jagani --- .../devtools/RequestFailedException.java | 29 +++++++++++++++++++ .../selenium/devtools/idealized/Network.java | 18 ++++++++++-- .../devtools/NetworkInterceptorTest.java | 23 ++++++++++++++- 3 files changed, 67 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 0000000000000..6ffef0c946fc3 --- /dev/null +++ b/java/src/org/openqa/selenium/devtools/RequestFailedException.java @@ -0,0 +1,29 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 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 2c09e877a7973..ec36b5d40d5d0 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 f916a1772c115..2999f1666c099 100644 --- a/java/test/org/openqa/selenium/devtools/NetworkInterceptorTest.java +++ b/java/test/org/openqa/selenium/devtools/NetworkInterceptorTest.java @@ -254,11 +254,32 @@ 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!"); + } + } }