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