Skip to content

Commit 801af5f

Browse files
committed
fixup! STF-322: Add bounded transport-failure retry to WebServiceClient
1 parent a9b9e2a commit 801af5f

1 file changed

Lines changed: 22 additions & 15 deletions

File tree

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
import java.net.http.HttpRequest;
3232
import java.net.http.HttpResponse;
3333
import java.net.http.HttpTimeoutException;
34+
import java.nio.channels.ClosedChannelException;
3435
import java.nio.charset.StandardCharsets;
3536
import java.time.Duration;
3637
import java.util.Base64;
3738
import java.util.HashMap;
3839
import java.util.List;
40+
import java.util.Locale;
3941
import java.util.Map;
4042

4143
/**
@@ -423,20 +425,17 @@ private <T> T responseFor(String path, InetAddress ipAddress, Class<T> cls)
423425

424426
private HttpResponse<InputStream> sendWithRetry(HttpRequest request)
425427
throws IOException, InterruptedException {
426-
IOException lastException = null;
427-
int attempts = maxRetries + 1;
428-
for (int i = 0; i < attempts; i++) {
428+
int attempts = 0;
429+
while (true) {
429430
try {
430431
return httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
431432
} catch (IOException e) {
432-
if (!isRetriableTransportFailure(e) || i == attempts - 1) {
433+
if (!isRetriableTransportFailure(e) || attempts >= maxRetries) {
433434
throw e;
434435
}
435-
lastException = e;
436+
attempts++;
436437
}
437438
}
438-
// Unreachable: loop either returns or throws.
439-
throw lastException;
440439
}
441440

442441
private static boolean isRetriableTransportFailure(IOException e) {
@@ -445,11 +444,14 @@ private static boolean isRetriableTransportFailure(IOException e) {
445444
}
446445
// Walk the cause chain: the JDK HttpClient wraps the underlying transport
447446
// failure in different ways depending on the protocol path. Over HTTP/1.1
448-
// a "Connection reset" surfaces as a SocketException; over HTTP/2 (e.g.
449-
// a SETTINGS-frame write failure) it may surface as a plain IOException
450-
// with the same message. Match by message regardless of class to handle
451-
// both, while keeping the type checks for connect-phase timeouts and
452-
// request-phase timeouts (which must NEVER be retried).
447+
// a "Connection reset" surfaces as a SocketException; over HTTP/2 a
448+
// SETTINGS-frame write failure may surface as a plain IOException with
449+
// the same message; an HTTP/2 upgrade against an already-closed socket
450+
// surfaces as a (message-less) IOException caused by a
451+
// ClosedChannelException. Match by class for the framing types and by
452+
// message (case-insensitive) for the resets, while keeping the explicit
453+
// checks for connect-phase timeouts and request-phase timeouts (which
454+
// must NEVER be retried).
453455
Throwable t = e;
454456
while (t != null) {
455457
if (t instanceof HttpConnectTimeoutException) {
@@ -461,11 +463,16 @@ private static boolean isRetriableTransportFailure(IOException e) {
461463
if (t instanceof ConnectException) {
462464
return true;
463465
}
464-
String msg = t.getMessage();
465-
if (msg != null
466-
&& (msg.contains("Connection reset") || msg.contains("Broken pipe"))) {
466+
if (t instanceof ClosedChannelException) {
467467
return true;
468468
}
469+
String msg = t.getMessage();
470+
if (msg != null) {
471+
String lower = msg.toLowerCase(Locale.ROOT);
472+
if (lower.contains("connection reset") || lower.contains("broken pipe")) {
473+
return true;
474+
}
475+
}
469476
t = t.getCause();
470477
}
471478
return false;

0 commit comments

Comments
 (0)