1717import java .io .IOException ;
1818import java .io .InputStream ;
1919import java .io .InterruptedIOException ;
20+ import java .net .ConnectException ;
2021import java .net .ProxySelector ;
2122import java .net .URI ;
2223import java .net .URISyntaxException ;
24+ import java .net .UnknownHostException ;
2325import java .net .http .HttpClient ;
2426import java .net .http .HttpRequest ;
2527import java .net .http .HttpResponse ;
3133import java .util .HashMap ;
3234import java .util .List ;
3335import java .util .Map ;
36+ import javax .net .ssl .SSLHandshakeException ;
37+ import javax .net .ssl .SSLPeerUnverifiedException ;
3438
3539/**
3640 * Client for MaxMind minFraud Score, Insights, and Factors
@@ -124,13 +128,8 @@ public Builder(int accountId, String licenseKey) {
124128 /**
125129 * @param val Timeout duration to establish a connection to the web service. There is no
126130 * timeout by default.
127- * <p>
128- * When {@code maxRetries > 0}, one API call may incur up to
129- * {@code (maxRetries + 1)} connection attempts, each subject to
130- * {@code connectTimeout} and {@code requestTimeout}. Worst-case wall-clock
131- * duration is roughly
132- * {@code (maxRetries + 1) x (connectTimeout + requestTimeout)}.
133131 * @return Builder object
132+ * @apiNote See {@link #maxRetries(int)} for how this timeout interacts with retries.
134133 */
135134 public WebServiceClient .Builder connectTimeout (Duration val ) {
136135 connectTimeout = val ;
@@ -184,14 +183,9 @@ public WebServiceClient.Builder locales(List<String> val) {
184183
185184
186185 /**
187- * @param val Request timeout duration. here is no timeout by default.
188- * <p>
189- * When {@code maxRetries > 0}, one API call may incur up to
190- * {@code (maxRetries + 1)} connection attempts, each subject to
191- * {@code connectTimeout} and {@code requestTimeout}. Worst-case wall-clock
192- * duration is roughly
193- * {@code (maxRetries + 1) x (connectTimeout + requestTimeout)}.
186+ * @param val Request timeout duration. There is no timeout by default.
194187 * @return Builder object
188+ * @apiNote See {@link #maxRetries(int)} for how this timeout interacts with retries.
195189 */
196190 public Builder requestTimeout (Duration val ) {
197191 requestTimeout = val ;
@@ -225,11 +219,20 @@ public Builder httpClient(HttpClient val) {
225219
226220 /**
227221 * @param val Maximum number of retries on transport-level failures
228- * (connection reset, broken pipe, connect timeout , ...).
222+ * (connection reset, broken pipe, EOF , ...).
229223 * Applies uniformly to all endpoints. Defaults to 1.
230224 * Set to 0 to disable.
231225 * @return Builder.
232226 * @throws IllegalArgumentException if {@code val} is negative.
227+ * @apiNote Timeouts are not retried ({@link java.net.http.HttpTimeoutException},
228+ * including the connect-phase subclass
229+ * {@link java.net.http.HttpConnectTimeoutException}). When
230+ * {@code maxRetries > 0}, retries are triggered only by fast transport
231+ * failures, so each attempt is independently bounded by
232+ * {@link #connectTimeout(Duration)} and {@link #requestTimeout(Duration)}.
233+ * The multiplied worst-case wall clock a naive reading suggests is
234+ * unreachable in practice, since hitting the timeout aborts the call
235+ * rather than triggering a retry.
233236 */
234237 public Builder maxRetries (int val ) {
235238 if (val < 0 ) {
@@ -386,13 +389,18 @@ private <T> T responseFor(String service, AbstractModel transaction, Class<T> cl
386389 private HttpResponse <InputStream > sendWithRetry (HttpRequest request )
387390 throws IOException , InterruptedException {
388391 int attempts = 0 ;
392+ IOException prior = null ;
389393 while (true ) {
390394 try {
391395 return httpClient .send (request , HttpResponse .BodyHandlers .ofInputStream ());
392396 } catch (IOException e ) {
397+ if (prior != null ) {
398+ e .addSuppressed (prior );
399+ }
393400 if (!isRetriableTransportFailure (e ) || attempts >= maxRetries ) {
394401 throw e ;
395402 }
403+ prior = e ;
396404 attempts ++;
397405 }
398406 }
@@ -413,11 +421,24 @@ private static boolean isRetriableTransportFailure(IOException e) {
413421 if (e instanceof InterruptedIOException ) {
414422 return false ;
415423 }
424+ // Typically deterministic failures: retrying just delays surfacing the
425+ // config bug without recovering the request.
426+ if (e instanceof UnknownHostException ) {
427+ return false ;
428+ }
429+ if (e instanceof ConnectException ) {
430+ return false ;
431+ }
432+ if (e instanceof SSLHandshakeException ) {
433+ return false ;
434+ }
435+ if (e instanceof SSLPeerUnverifiedException ) {
436+ return false ;
437+ }
416438 // Everything else from httpClient.send() is a transport failure
417- // (connection reset, broken pipe, EOF, closed channel, connect refused,
418- // DNS, TLS, protocol). HTTP 4xx and 5xx responses cannot reach this
419- // predicate -- they come back as HttpResponse objects rather than
420- // IOExceptions.
439+ // (connection reset, broken pipe, EOF, closed channel, ...).
440+ // HTTP 4xx and 5xx responses do not reach this predicate -- they come
441+ // back as HttpResponse objects rather than IOExceptions.
421442 return true ;
422443 }
423444
0 commit comments