@@ -635,9 +635,8 @@ public void testNoRetryOn4xx() {
635635 // as plain HTTP/1.1. The HTTP/1.1 path then triggers the JDK's own
636636 // idempotent-GET retry inside HttpClient.send(), producing two wire
637637 // requests where the test expects one. This is platform-specific JDK
638- // behavior we cannot disable from application code; the equivalent
639- // POST-based test in minfraud-api-java is unaffected because the JDK
640- // does not internally retry non-idempotent requests.
638+ // behavior we cannot disable from application code; only idempotent
639+ // methods (GET / HEAD) are affected.
641640 @ Test
642641 @ DisabledOnOs (OS .WINDOWS )
643642 public void testMaxRetriesZeroDisablesRetry () {
@@ -658,9 +657,12 @@ public void testMaxRetriesZeroDisablesRetry() {
658657 wireMock .verify (1 , getRequestedFor (urlEqualTo (url )));
659658 }
660659
661- // Disabled on Windows for the same reason as testMaxRetriesZeroDisablesRetry:
662- // the JDK's internal idempotent-GET retry can stack on top of our retry
663- // loop on the HTTP/1.1 fallback path, multiplying wire counts.
660+ // Disabled on Windows: the JDK's internal idempotent-GET retry on the
661+ // HTTP/1.1 fallback path (triggered by Windows-specific h2c upgrade
662+ // failures against an immediate RST) stacks on top of our retry loop,
663+ // multiplying wire counts (each of our 3 attempts becomes 2 wire
664+ // requests, so the count assertion sees 6 instead of 3). This is JDK
665+ // behavior we cannot disable from application code.
664666 @ Test
665667 @ DisabledOnOs (OS .WINDOWS )
666668 public void testRetriesExhausted () {
@@ -675,11 +677,51 @@ public void testRetriesExhausted() {
675677 .maxRetries (2 )
676678 .build ();
677679
678- assertThrows (IOException .class ,
680+ IOException ex = assertThrows (IOException .class ,
679681 () -> client .insights (InetAddress .getByName ("1.2.3.4" )));
680682
681683 // 1 initial attempt + 2 retries.
682684 wireMock .verify (3 , getRequestedFor (urlEqualTo (url )));
685+ // The final exception should carry the prior failures as suppressed
686+ // exceptions so the full retry history is visible in stack traces.
687+ assertEquals (2 , ex .getSuppressed ().length ,
688+ "expected the 2 prior IOExceptions to be attached as suppressed" );
689+ }
690+
691+ @ Test
692+ public void testCustomHttpClientStillRetries () throws Exception {
693+ // The Javadoc on Builder.httpClient(HttpClient) promises that the SDK's
694+ // transport-failure retry wraps any supplied client. Verify it.
695+ String url = "/geoip/v2.1/country/1.2.3.4" ;
696+ String body = "{\" traits\" :{\" ip_address\" :\" 1.2.3.4\" }}" ;
697+
698+ wireMock .stubFor (get (urlEqualTo (url ))
699+ .inScenario ("retry-custom-client" )
700+ .whenScenarioStateIs (Scenario .STARTED )
701+ .willReturn (aResponse ().withFault (Fault .CONNECTION_RESET_BY_PEER ))
702+ .willSetStateTo ("succeeded" ));
703+
704+ wireMock .stubFor (get (urlEqualTo (url ))
705+ .inScenario ("retry-custom-client" )
706+ .whenScenarioStateIs ("succeeded" )
707+ .willReturn (aResponse ()
708+ .withStatus (200 )
709+ .withHeader ("Content-Type" ,
710+ "application/vnd.maxmind.com-country+json; charset=UTF-8; version=2.1" )
711+ .withBody (body )));
712+
713+ HttpClient customClient = HttpClient .newBuilder ().build ();
714+ WebServiceClient client = new WebServiceClient .Builder (6 , "0123456789" )
715+ .host ("localhost" )
716+ .port (wireMock .getPort ())
717+ .disableHttps ()
718+ .httpClient (customClient )
719+ .build ();
720+
721+ CountryResponse response = client .country (InetAddress .getByName ("1.2.3.4" ));
722+ assertNotNull (response );
723+
724+ wireMock .verify (2 , getRequestedFor (urlEqualTo (url )));
683725 }
684726
685727 @ Test
@@ -689,13 +731,17 @@ public void testNegativeMaxRetriesThrows() {
689731 }
690732
691733 @ Test
692- public void testInterruptDuringRetry () {
693- // Pre-interrupting the calling thread aborts the call before any wire
694- // request is dispatched: HttpClient.send checks the interrupt status
695- // and throws InterruptedException, which is caught and rewrapped as
696- // GeoIp2Exception with the interrupt flag restored. The wire-count
697- // assertion (zero) guards against a regression where pre-interrupt
698- // would silently let the request proceed.
734+ public void testInterruptedThreadAbortsBeforeSend () {
735+ // When the calling thread is already interrupted, HttpClient.send
736+ // checks the interrupt status and throws InterruptedException before
737+ // dispatching any wire request. The exception is caught and rewrapped
738+ // as GeoIp2Exception, with the interrupt flag restored on the calling
739+ // thread. The wire-count assertion (zero) guards against a regression
740+ // where a pre-interrupt would silently let the request proceed.
741+ // NOTE: this test does not exercise the predicate's own
742+ // Thread.currentThread().isInterrupted() short-circuit, since the JDK
743+ // aborts before that branch can be reached; a true mid-flight
744+ // interrupt is hard to test deterministically.
699745 String url = "/geoip/v2.1/insights/1.2.3.4" ;
700746 wireMock .stubFor (get (urlEqualTo (url ))
701747 .willReturn (aResponse ().withFault (Fault .CONNECTION_RESET_BY_PEER )));
0 commit comments