RFR: 8340182: Java HttpClient does not follow default retry limit of 3 retries
    Volkan Yazici 
    vyazici at openjdk.org
       
    Wed Jun  4 13:16:17 UTC 2025
    
    
  
On Wed, 28 May 2025 11:26:17 GMT, p-nima <duke at openjdk.org> wrote:
> The AuthenticationFilter did not respect the default retry limit of 3 retries in case of invalid credentials supplied.
> 
> This PR helps to resolve the bug and tests it with default and updated retry limit set via `jdk.httpclient.auth.retrylimit=1`.
> 
> The test is green with tiers 1, 2, 3 and the test is stable.
Changes requested by vyazici (Author).
@p-nima, thanks for quickly addressing the review feedback. I've dropped some further remarks.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 34:
> 32:  * @build jdk.httpclient.test.lib.http2.Http2TestServer
> 33:  * @run junit HttpClientRetryLimitTest
> 34:  * @run junit/othervm -Djdk.httpclient.auth.retrylimit=1 HttpClientRetryLimitTest
@dfuch, shall we test against zero and negative values too? (Both are accepted by `AuthenticationFilter` as valid.)
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 49:
> 47: import static jdk.test.lib.Asserts.assertEquals;
> 48: 
> 49: public class HttpClientRetryLimitTest implements HttpServerAdapters {
*Nit:* [JUnit recommends the following](https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-classes-and-methods)
> It is generally recommended to omit the public modifier for test classes, test methods, and lifecycle methods unless there is a technical reason for doing so ...
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 52:
> 50: 
> 51:     private static final int DEFAULT_RETRY_LIMIT = 3;
> 52:     private final int retryLimit = Integer.getInteger("jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);
This can be `static` too.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 53:
> 51:     private static final int DEFAULT_RETRY_LIMIT = 3;
> 52:     private final int retryLimit = Integer.getInteger("jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);
> 53:     private int countRetries;
This should better be converted to a local variable.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 57:
> 55: 
> 56: 
> 57: class HttpClientRetryLimitTest implements HttpServerAdapters {
Shall we rename this to `HttpClientAuthRetryLimit`, since [there are several other retry limits](https://docs.oracle.com/en/java/javase/23/docs/api/java.net.http/module-summary.html#jdk.httpclient.auth.retrylimit)?
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 58:
> 56:     public void testDefaultSystemProperty() throws Exception {
> 57: 
> 58:         try (HttpTestServer httpTestServer = HttpTestServer.create(HttpClient.Version.HTTP_1_1)) {
Don't we need to verify the behavior for HTTP/2 too? If so, can we use a `@ParameterizedTest` instead and receive the protocol version? (There are several examples you can get inspired from; `HttpResponseConnectionLabelTest`, `EmptyAuthenticate`, etc.)
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 64:
> 62:             "jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);
> 63: 
> 64:     static Stream<HttpClient.Version> args() {
I think we should also test against with SSL and without SSL cases. See `HttpResponseLimitingTest.ServerClientPair` for inspiration.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 66:
> 64:             };
> 65: 
> 66:             httpTestServer.addHandler(httpTestHandler, "/");
In the past, there has been occasions in CI where a test server received connections from a totally unrelated test running in parallel on the same host. To avoid such mishaps, we better salt the path a bit by, say, appending the class' simple name?
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 69:
> 67:             httpTestServer.start();
> 68: 
> 69:             countRetries = 0;
IMHO, this should be an `Atomic{Integer,Long}` due to the possibility of concurrent updates.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 73:
> 71:     @ParameterizedTest
> 72:     @MethodSource("args")
> 73:     public void testDefaultSystemProperty(HttpClient.Version version) throws Exception {
I see you made the class package-private in 18bac9f. You could have additionally made the method package-private too.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 87:
> 85:                         .build();
> 86: 
> 87:                 client.send(request, HttpResponse.BodyHandlers.ofString());
Since we're not interested in the response, `BodyHandlers.discarding()` might be a better fit here.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 88:
> 86:             try (
> 87:                 HttpClient client = HttpClient.newBuilder()
> 88:                         .authenticator(new Authenticator() {
To ensure the client will fire the request using the protocol version of our preference, could you also pass `version` to the client builder too, please?
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 92:
> 90:                         "Expected number of retries was " + retryLimit + " but actual was "+countRetries);
> 91:                 e.printStackTrace();
> 92:             }
AFAIU, you should
1. Wrap `client::send` with `assertThrows`
2. Verify the returned exception
1. Then `assertEquals` the retry count
Otherwise, if `client::send` _unexpectedly_ doesn't throw anything, your test will incorrectly succeed.
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 99:
> 97:                 HttpRequest request = HttpRequest.newBuilder()
> 98:                         .GET()
> 99:                         .uri(new URI("http://" + httpTestServer.serverAuthority() + "/" + this.getClass().getSimpleName() + "/"))
To ensure the client will fire the request using the protocol version of our preference, could you also pass `version` to the request builder too, please?
test/jdk/java/net/httpclient/HttpClientRetryLimitTest.java line 113:
> 111:                 }
> 112:                 e.printStackTrace();
> 113:             }
AFAICT, you should be using `assertThrows` as follows:
IOException exception = assertThrows(...);
assertEquals(exception.message(), "too many authentication attempts. Limit: " + RETRY_LIMIT);
assertEquals(requestCount.get(), RETRY_LIMIT > 0 ? RETRY_LIMIT : 1);
-------------
PR Review: https://git.openjdk.org/jdk/pull/25490#pullrequestreview-2874776142
Changes requested by vyazici (Author).
PR Review: https://git.openjdk.org/jdk/pull/25490#pullrequestreview-2880603038
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111767554
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111701285
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111701781
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111702335
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2116087627
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111743245
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2116060516
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111722490
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111704822
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2115470192
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111730359
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2115451397
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2111760843
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2115449940
PR Review Comment: https://git.openjdk.org/jdk/pull/25490#discussion_r2115466187
    
    
More information about the net-dev
mailing list