Skip to content

Commit 55778d4

Browse files
committed
fixup! STF-322: Add bounded transport-failure retry to WebServiceClient
1 parent 0dee88d commit 55778d4

1 file changed

Lines changed: 22 additions & 15 deletions

File tree

src/main/java/com/maxmind/minfraud/WebServiceClient.java

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
import java.net.http.HttpRequest;
2626
import java.net.http.HttpResponse;
2727
import java.net.http.HttpTimeoutException;
28+
import java.nio.channels.ClosedChannelException;
2829
import java.nio.charset.StandardCharsets;
2930
import java.time.Duration;
3031
import java.util.Base64;
3132
import java.util.Collections;
3233
import java.util.HashMap;
3334
import java.util.List;
35+
import java.util.Locale;
3436
import java.util.Map;
3537

3638
/**
@@ -386,20 +388,17 @@ private <T> T responseFor(String service, AbstractModel transaction, Class<T> cl
386388

387389
private HttpResponse<InputStream> sendWithRetry(HttpRequest request)
388390
throws IOException, InterruptedException {
389-
IOException lastException = null;
390-
int attempts = maxRetries + 1;
391-
for (int i = 0; i < attempts; i++) {
391+
int attempts = 0;
392+
while (true) {
392393
try {
393394
return httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
394395
} catch (IOException e) {
395-
if (!isRetriableTransportFailure(e) || i == attempts - 1) {
396+
if (!isRetriableTransportFailure(e) || attempts >= maxRetries) {
396397
throw e;
397398
}
398-
lastException = e;
399+
attempts++;
399400
}
400401
}
401-
// Unreachable: loop either returns or throws.
402-
throw lastException;
403402
}
404403

405404
private static boolean isRetriableTransportFailure(IOException e) {
@@ -408,11 +407,14 @@ private static boolean isRetriableTransportFailure(IOException e) {
408407
}
409408
// Walk the cause chain: the JDK HttpClient wraps the underlying transport
410409
// failure in different ways depending on the protocol path. Over HTTP/1.1
411-
// a "Connection reset" surfaces as a SocketException; over HTTP/2 (e.g.
412-
// a SETTINGS-frame write failure) it may surface as a plain IOException
413-
// with the same message. Match by message regardless of class to handle
414-
// both, while keeping the type checks for connect-phase timeouts and
415-
// request-phase timeouts (which must NEVER be retried).
410+
// a "Connection reset" surfaces as a SocketException; over HTTP/2 a
411+
// SETTINGS-frame write failure may surface as a plain IOException with
412+
// the same message; an HTTP/2 upgrade against an already-closed socket
413+
// surfaces as a (message-less) IOException caused by a
414+
// ClosedChannelException. Match by class for the framing types and by
415+
// message (case-insensitive) for the resets, while keeping the explicit
416+
// checks for connect-phase timeouts and request-phase timeouts (which
417+
// must NEVER be retried).
416418
Throwable t = e;
417419
while (t != null) {
418420
if (t instanceof HttpConnectTimeoutException) {
@@ -424,11 +426,16 @@ private static boolean isRetriableTransportFailure(IOException e) {
424426
if (t instanceof ConnectException) {
425427
return true;
426428
}
427-
String msg = t.getMessage();
428-
if (msg != null
429-
&& (msg.contains("Connection reset") || msg.contains("Broken pipe"))) {
429+
if (t instanceof ClosedChannelException) {
430430
return true;
431431
}
432+
String msg = t.getMessage();
433+
if (msg != null) {
434+
String lower = msg.toLowerCase(Locale.ROOT);
435+
if (lower.contains("connection reset") || lower.contains("broken pipe")) {
436+
return true;
437+
}
438+
}
432439
t = t.getCause();
433440
}
434441
return false;

0 commit comments

Comments
 (0)