Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/HttpCall.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
import java.net.http.HttpTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
Expand All @@ -60,7 +60,7 @@
* The request takes no thread while it waits. Its timeout counts from the
* moment it is made and bounds it as a whole, redirects included: the head of
* the response by the client's own request timeout, set to what is left, and
* the body read by read, so that one that trickles in ends with it as well.
* an independent deadline through the body, including when no more data arrives.
* {@link #abort()} ends it from any thread: a body being read at once, and on
* Java 16 and later a request still waiting for its head too; before that,
* such a request ends with its timeout.
Expand Down Expand Up @@ -248,7 +248,23 @@ HttpCall followRedirects() {
* passed.
*/
CompletableFuture<Response> send(String contentType, byte[] content, int maxBody, Consumer<Response> onHead) {
exchange(uri, contentType, content, maxBody, onHead, 0);
// The client's timeout ends at the response head. Keep a deadline
// through the body and retries, even when the server goes silent.
// Completing this guard removes its timeout from the JDK's timer.
CompletableFuture<Void> deadline = new CompletableFuture<>();
deadline.orTimeout(Math.max(0, remainingNanos()), TimeUnit.NANOSECONDS)
.exceptionally(error -> {
abort();
return null;
});
result.whenComplete((response, error) -> deadline.complete(null));

try {
exchange(uri, contentType, content, maxBody, onHead, 0);
}
catch (RuntimeException e) {
result.completeExceptionally(e);
}

return result;
}
Expand Down Expand Up @@ -369,6 +385,11 @@ private void exchange(URI target, String contentType, byte[] content, int maxBod
catch (IOException e) {
result.completeExceptionally(e);
}
}).exceptionally(error -> {
// A failure while processing a response must reach the caller,
// rather than only the discarded completion stage.
result.completeExceptionally(unwrap(error));
return null;
});
}

Expand Down Expand Up @@ -466,9 +487,9 @@ static long retryDelayMillis(String value, long nowMillis) {
try {
long date = ZonedDateTime.parse(field, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant().toEpochMilli();

return Math.max(0, date - nowMillis);
return Math.max(0, Math.subtractExact(date, nowMillis));
}
catch (DateTimeParseException e) {
catch (DateTimeException | ArithmeticException e) {
return -1;
}
}
Expand Down
49 changes: 49 additions & 0 deletions webrtc/src/test/java/dev/onvoid/webrtc/client/HttpCallTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpTimeoutException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
Expand All @@ -41,10 +42,13 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLHandshakeException;
Expand Down Expand Up @@ -131,6 +135,27 @@ void oversizedAnswerRefused() throws Exception {
}
}

@Test
void deadlineEndsSilentBody() throws Exception {
try (TestHttpEndpoint endpoint = new TestHttpEndpoint(offer -> "v=0 and the rest", 201)) {
endpoint.stallBodies();
CountDownLatch head = new CountDownLatch(1);
HttpCall call = new HttpCall(HttpCall.newClient(false), endpoint.uri(), "POST", null,
Duration.ofSeconds(2));
CompletableFuture<HttpCall.Response> response = call.send("application/sdp", new byte[] { 'o' },
1024, received -> head.countDown());
try {
assertTrue(head.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
ExecutionException failed = assertThrows(ExecutionException.class, () -> await(response));
assertInstanceOf(HttpTimeoutException.class, failed.getCause());
endpoint.awaitHangUp(TIMEOUT_SECONDS);
}
finally {
call.abort();
}
}
}

@Test
void abortEndsRead() throws Exception {
// The server sends the head and half the answer, then waits. An abort
Expand Down Expand Up @@ -276,6 +301,29 @@ void tunnelRefusesBasicByDefault() throws Exception {
}
}

@Test
void overflowingRetryDateReturnsResponse() throws Exception {
ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
try (TestHttpEndpoint endpoint = new TestHttpEndpoint(offer -> "come back later", 201)) {
endpoint.answerStatus = 503;
endpoint.answerHeaders = "Retry-After: 31 Dec 999999999 23:59:59 GMT\r\n";
HttpCall call = post(HttpCall.newClient(false), endpoint.uri()).retry(timer, Set.of(503));
try {
HttpCall.Response response = await(call.send("application/sdp", new byte[] { 'o' }, 1024,
head -> { }));
assertEquals(503, response.status);
assertEquals("come back later", response.body);
assertEquals(1, endpoint.requests.size());
}
finally {
call.abort();
}
}
finally {
timer.shutdownNow();
}
}

@Test
void retryAfterRead() {
long now = 1_000_000_000_000L;
Expand All @@ -289,6 +337,7 @@ void retryAfterRead() {
assertEquals(5000, HttpCall.retryDelayMillis(inFive, now));
assertEquals(0, HttpCall.retryDelayMillis(past, now));
assertEquals(Long.MAX_VALUE, HttpCall.retryDelayMillis("99999999999999999999", now));
assertEquals(-1, HttpCall.retryDelayMillis("31 Dec 999999999 23:59:59 GMT", now));
assertEquals(-1, HttpCall.retryDelayMillis("soon", now));
assertEquals(-1, HttpCall.retryDelayMillis("", now));
assertEquals(-1, HttpCall.retryDelayMillis(null, now));
Expand Down
Loading