Skip to content

8340182: Java HttpClient does not follow default retry limit of 3 retries #25490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

p-nima
Copy link

@p-nima p-nima commented May 28, 2025

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.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Error

 ⚠️ OCA signatory status must be verified

Issue

  • JDK-8340182: Java HttpClient does not follow default retry limit of 3 retries (Bug - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/25490/head:pull/25490
$ git checkout pull/25490

Update a local copy of the PR:
$ git checkout pull/25490
$ git pull https://git.openjdk.org/jdk.git pull/25490/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 25490

View PR using the GUI difftool:
$ git pr show -t 25490

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/25490.diff

@bridgekeeper bridgekeeper bot added the oca Needs verification of OCA signatory status label May 28, 2025
@bridgekeeper
Copy link

bridgekeeper bot commented May 28, 2025

Hi @p-nima, welcome to this OpenJDK project and thanks for contributing!

We do not recognize you as Contributor and need to ensure you have signed the Oracle Contributor Agreement (OCA). If you have not signed the OCA, please follow the instructions. Please fill in your GitHub username in the "Username" field of the application. Once you have signed the OCA, please let us know by writing /signed in a comment in this pull request.

If you already are an OpenJDK Author, Committer or Reviewer, please click here to open a new issue so that we can record that fact. Please use "Add GitHub user p-nima" as summary for the issue.

If you are contributing this work on behalf of your employer and your employer has signed the OCA, please let us know by writing /covered in a comment in this pull request.

@openjdk
Copy link

openjdk bot commented May 28, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk
Copy link

openjdk bot commented May 28, 2025

@p-nima The following label will be automatically applied to this pull request:

  • net

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk bot added the net net-dev@openjdk.org label May 28, 2025
@p-nima
Copy link
Author

p-nima commented May 28, 2025

/covered

@bridgekeeper bridgekeeper bot added the oca-verify Needs verification of OCA signatory status label May 28, 2025
@bridgekeeper
Copy link

bridgekeeper bot commented May 28, 2025

Thank you! Please allow for a few business days to verify that your employer has signed the OCA. Also, please note that pull requests that are pending an OCA check will not usually be evaluated, so your patience is appreciated!

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static jdk.test.lib.Asserts.assertEquals;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: Did you mean import static org.junit.jupiter.api.Assertions.assertEquals; here, as you're using junit?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review. The changes have been made in 18bac9f

public class HttpClientRetryLimitTest implements HttpServerAdapters {

private static final int DEFAULT_RETRY_LIMIT = 3;
private final int retryLimit = Integer.getInteger("jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This feels a bit confusing to me when reading it the first time, why not have a retry limit of 1 or 0 as a default and then specify if you want more retries in @test?
I think it might be a bit easier to read, but if you want to keep it, it's fine with me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is grabbing the value of the jdk.httpclient.auth.retrylimit system property, and if it hasn't been set then it defaults to 3. This is based on AuthenticationFilter.java

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment has been added for it in cdf7812

* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit HttpClientRetryLimitTest
* @run junit/othervm -Djdk.httpclient.auth.retrylimit=1 HttpClientRetryLimitTest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think adding a retrylimit=0 would be beneficial too? This way every scenario would be covered

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added additional test case scenarios in 18bac9f


import static jdk.test.lib.Asserts.assertEquals;

public class HttpClientRetryLimitTest implements HttpServerAdapters {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: JUnit recommends the following

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 ...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your feedback. The public modifier has been discarded in 18bac9f

public class HttpClientRetryLimitTest implements HttpServerAdapters {

private static final int DEFAULT_RETRY_LIMIT = 3;
private final int retryLimit = Integer.getInteger("jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be static too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this in 18bac9f


private static final int DEFAULT_RETRY_LIMIT = 3;
private final int retryLimit = Integer.getInteger("jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);
private int countRetries;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should better be converted to a local variable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discarded the above local variable and introduced a AtomicInteger variable in 18bac9f

httpTestServer.addHandler(httpTestHandler, "/");
httpTestServer.start();

countRetries = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, this should be an Atomic{Integer,Long} due to the possibility of concurrent updates.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This has been updated in 18bac9f

t.sendResponseHeaders(401,0);
};

httpTestServer.addHandler(httpTestHandler, "/");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch - the class simple name has been appended in 18bac9f

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@p-nima, the handler still accepts all calls to /, you only salted the request URI. Would you mind doing the same in httpTestServer.addHandler(...) too, please? (You better create a String requestPath variable and use it both at the handler and the request.)

.uri(new URI("http://" + httpTestServer.serverAuthority() + "/"))
.build();

client.send(request, HttpResponse.BodyHandlers.ofString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not interested in the response, BodyHandlers.discarding() might be a better fit here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this in 18bac9f

@Test
public void testDefaultSystemProperty() throws Exception {

try (HttpTestServer httpTestServer = HttpTestServer.create(HttpClient.Version.HTTP_1_1)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Comment on lines 88 to 92
} catch (Exception e) {
assertEquals(retryLimit, countRetries,
"Expected number of retries was " + retryLimit + " but actual was "+countRetries);
e.printStackTrace();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU, you should

  1. Wrap client::send with assertThrows
  2. Verify the returned exception
  3. Then assertEquals the retry count

Otherwise, if client::send unexpectedly doesn't throw anything, your test will incorrectly succeed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed - the changes have been made in 18bac9f

* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* @run junit HttpClientRetryLimitTest
* @run junit/othervm -Djdk.httpclient.auth.retrylimit=1 HttpClientRetryLimitTest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dfuch, shall we test against zero and negative values too? (Both are accepted by AuthenticationFilter as valid.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could. We could also throw in garbage to see what happens.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test scenarios have been added in 18bac9f. We have default, positive, zero and negative values.

Copy link
Contributor

@vy vy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@p-nima, thanks for quickly addressing the review feedback. I've dropped some further remarks.

t.sendResponseHeaders(401,0);
};

httpTestServer.addHandler(httpTestHandler, "/");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@p-nima, the handler still accepts all calls to /, you only salted the request URI. Would you mind doing the same in httpTestServer.addHandler(...) too, please? (You better create a String requestPath variable and use it both at the handler and the request.)


HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(new URI("http://" + httpTestServer.serverAuthority() + "/" + this.getClass().getSimpleName() + "/"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

httpTestServer.start();
try (
HttpClient client = HttpClient.newBuilder()
.authenticator(new Authenticator() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines +101 to +113
throw assertThrows(IOException.class, () -> client.send(request, HttpResponse.BodyHandlers.discarding()));
} catch (Exception e) {
if (RETRY_LIMIT > 0){
assertEquals(RETRY_LIMIT, requestCount.get(),
"Expected number of request retries was " + RETRY_LIMIT + " but actual was "+requestCount);
}
else{
requestCount.decrementAndGet();
assertEquals(0, requestCount.get(),
"Expected number of request retries was 0 but actual was "+requestCount);
}
e.printStackTrace();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);


@ParameterizedTest
@MethodSource("args")
public void testDefaultSystemProperty(HttpClient.Version version) throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you made the class package-private in 18bac9f. You could have additionally made the method package-private too.

private static final int RETRY_LIMIT = Integer.getInteger(
"jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);

static Stream<HttpClient.Version> args() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also test against with SSL and without SSL cases. See HttpResponseLimitingTest.ServerClientPair for inspiration.

import static org.junit.jupiter.api.Assertions.assertThrows;


class HttpClientRetryLimitTest implements HttpServerAdapters {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we rename this to HttpClientAuthRetryLimit, since there are several other retry limits?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable to rename - and fix the @summary in the jtreg headers too

@jaikiran
Copy link
Member

/touch

@openjdk
Copy link

openjdk bot commented May 30, 2025

@jaikiran The pull request is being re-evaluated and the inactivity timeout has been reset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
net net-dev@openjdk.org oca Needs verification of OCA signatory status oca-verify Needs verification of OCA signatory status
Development

Successfully merging this pull request may close these issues.

6 participants