Skip to content

Commit 78b6ea4

Browse files
committed
[java] allow to cancel a pending http request
1 parent 8aee746 commit 78b6ea4

File tree

2 files changed

+85
-24
lines changed

2 files changed

+85
-24
lines changed

java/src/org/openqa/selenium/remote/http/jdk/JdkHttpClient.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -388,17 +388,16 @@ public CompletableFuture<HttpResponse> executeAsync(HttpRequest request) {
388388
}
389389
});
390390

391-
// try to interrupt the http request in case of a timeout, to avoid
392-
// https://bugs.openjdk.org/browse/JDK-8258397
393-
cf.exceptionally(
394-
(throwable) -> {
395-
if (throwable instanceof java.util.concurrent.TimeoutException) {
396-
// interrupts the thread
391+
cf.whenComplete(
392+
(result, throwable) -> {
393+
if (throwable instanceof java.util.concurrent.CancellationException) {
394+
// try to interrupt the http request in case someone canceled the future returned
395+
future.cancel(true);
396+
} else if (throwable instanceof java.util.concurrent.TimeoutException) {
397+
// try to interrupt the http request in case of a timeout, to avoid
398+
// https://bugs.openjdk.org/browse/JDK-8258397
397399
future.cancel(true);
398400
}
399-
400-
// nobody will read this result
401-
return null;
402401
});
403402

404403
// will complete exceptionally with a java.util.concurrent.TimeoutException
@@ -407,13 +406,15 @@ public CompletableFuture<HttpResponse> executeAsync(HttpRequest request) {
407406

408407
@Override
409408
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
409+
Future<HttpResponse> async = executeAsync(req);
410410
try {
411411
// executeAsync does define a timeout, no need to use a timeout here
412-
return executeAsync(req).get();
412+
return async.get();
413413
} catch (CancellationException e) {
414414
throw new WebDriverException(e.getMessage(), e);
415415
} catch (InterruptedException e) {
416416
Thread.currentThread().interrupt();
417+
async.cancel(true);
417418
throw new WebDriverException(e.getMessage(), e);
418419
} catch (ExecutionException e) {
419420
Throwable cause = e.getCause();
@@ -495,7 +496,7 @@ private HttpResponse execute0(HttpRequest req) throws UncheckedIOException {
495496
throw new UncheckedIOException(e);
496497
} catch (InterruptedException e) {
497498
Thread.currentThread().interrupt();
498-
throw new RuntimeException(e);
499+
throw new WebDriverException(e.getMessage(), e);
499500
} finally {
500501
LOG.log(
501502
Level.FINE,

java/test/org/openqa/selenium/remote/internal/HttpClientTestBase.java

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
import java.util.List;
3636
import java.util.Map;
3737
import java.util.TreeMap;
38-
import java.util.concurrent.atomic.AtomicInteger;
38+
import java.util.concurrent.CountDownLatch;
39+
import java.util.concurrent.Future;
40+
import java.util.concurrent.TimeUnit;
3941
import java.util.logging.Logger;
4042
import java.util.stream.StreamSupport;
4143
import org.junit.jupiter.api.AfterAll;
@@ -44,6 +46,7 @@
4446
import org.openqa.selenium.BuildInfo;
4547
import org.openqa.selenium.Platform;
4648
import org.openqa.selenium.TimeoutException;
49+
import org.openqa.selenium.WebDriverException;
4750
import org.openqa.selenium.environment.webserver.AppServer;
4851
import org.openqa.selenium.environment.webserver.NettyAppServer;
4952
import org.openqa.selenium.json.Json;
@@ -234,32 +237,89 @@ public void shouldAllowConfigurationFromSystemProperties() {
234237
}
235238
}
236239

237-
@Test
238-
public void shouldStopRequestAfterTimeout() throws InterruptedException {
239-
AtomicInteger counter = new AtomicInteger();
240+
private ClientConfig prepareShouldStopTest(
241+
CountDownLatch executing, CountDownLatch interrupted, int timeout) {
242+
CountDownLatch unlock = new CountDownLatch(1);
240243

241244
delegate =
242245
req -> {
243-
counter.incrementAndGet();
244246
try {
245-
Thread.sleep(1600);
247+
unlock.await(20, TimeUnit.SECONDS);
246248
} catch (InterruptedException ex) {
247249
Thread.currentThread().interrupt();
250+
throw new RuntimeException(ex);
248251
}
249-
HttpResponse response = new HttpResponse();
250-
response.setStatus(302);
251-
response.addHeader("Location", "/");
252-
return response;
252+
253+
return new HttpResponse();
253254
};
254-
ClientConfig clientConfig = ClientConfig.defaultConfig().readTimeout(Duration.ofMillis(800));
255+
256+
return ClientConfig.defaultConfig()
257+
.withFilter(
258+
(handler) ->
259+
(request) -> {
260+
try {
261+
executing.countDown();
262+
return handler.execute(request);
263+
} catch (WebDriverException ex) {
264+
if (ex.getCause() instanceof InterruptedException) {
265+
interrupted.countDown();
266+
}
267+
268+
throw ex;
269+
} finally {
270+
unlock.countDown();
271+
}
272+
})
273+
.readTimeout(Duration.ofMillis(timeout));
274+
}
275+
276+
@Test
277+
public void shouldStopRequestAfterTimeout() throws InterruptedException {
278+
CountDownLatch executing = new CountDownLatch(1);
279+
CountDownLatch interrupted = new CountDownLatch(1);
280+
ClientConfig clientConfig = prepareShouldStopTest(executing, interrupted, 400);
255281

256282
try (HttpClient client =
257283
createFactory().createClient(clientConfig.baseUri(URI.create(server.whereIs("/"))))) {
258284
HttpRequest request = new HttpRequest(GET, "/delayed");
285+
259286
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> client.execute(request));
260-
Thread.sleep(4200);
287+
assertThat(interrupted.await(800, TimeUnit.MILLISECONDS)).isTrue();
288+
}
289+
}
290+
291+
@Test
292+
public void shouldStopAsyncRequestAfterTimeout() throws InterruptedException {
293+
CountDownLatch executing = new CountDownLatch(1);
294+
CountDownLatch interrupted = new CountDownLatch(1);
295+
ClientConfig clientConfig = prepareShouldStopTest(executing, interrupted, 400);
296+
297+
try (HttpClient client =
298+
createFactory().createClient(clientConfig.baseUri(URI.create(server.whereIs("/"))))) {
299+
HttpRequest request = new HttpRequest(GET, "/delayed");
300+
// does intentionally not read the future
301+
client.executeAsync(request);
302+
assertThat(interrupted.await(800, TimeUnit.MILLISECONDS)).isTrue();
303+
}
304+
}
305+
306+
@Test
307+
public void shouldStopRequestOnCancel() throws InterruptedException {
308+
CountDownLatch executing = new CountDownLatch(1);
309+
CountDownLatch interrupted = new CountDownLatch(1);
310+
CountDownLatch unlock = new CountDownLatch(1);
311+
ClientConfig clientConfig = prepareShouldStopTest(executing, interrupted, 4000);
312+
313+
try (HttpClient client =
314+
createFactory().createClient(clientConfig.baseUri(URI.create(server.whereIs("/"))))) {
315+
HttpRequest request = new HttpRequest(GET, "/delayed");
316+
317+
Future<?> future = client.executeAsync(request);
261318

262-
assertThat(counter.get()).isEqualTo(1);
319+
assertThat(executing.await(800, TimeUnit.MILLISECONDS)).isTrue();
320+
assertThat(future.cancel(true)).isTrue();
321+
assertThat(interrupted.await(800, TimeUnit.MILLISECONDS)).isTrue();
322+
unlock.countDown();
263323
}
264324
}
265325

0 commit comments

Comments
 (0)