From 287d1be327db68edcad795d53266275ab62654c1 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:47:55 +0200 Subject: [PATCH 1/2] Cache Basic auth header on Realm Cache the generated HTTP Basic authorization header on immutable Realm instances so repeated preemptive and proxy Basic auth requests can reuse the same header string. This avoids rebuilding credentials and Base64 output on every dispatch. Add a regression test that verifies repeated per-request Basic auth header generation returns the cached value. Codex on behalf of Pavel Ptashyts Co-Authored-By: Codex --- .../main/java/org/asynchttpclient/Realm.java | 17 +++++++++++++++++ .../util/AuthenticatorUtils.java | 8 +------- .../util/AuthenticatorUtilsTest.java | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index cf770dafe..7b2951776 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -25,6 +25,7 @@ import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.SecureRandom; +import java.util.Base64; import java.util.Map; import static java.nio.charset.StandardCharsets.ISO_8859_1; @@ -70,6 +71,7 @@ public class Realm { private final boolean userhash; private final @Nullable String sid; private final int maxIterationCount; + private @Nullable String basicAuthHeader; private Realm(@Nullable AuthScheme scheme, @Nullable String principal, @@ -245,6 +247,21 @@ public int getMaxIterationCount() { return maxIterationCount; } + /** + * Returns the HTTP Basic authorization header for this immutable realm. + * + * @return the Basic authorization header + */ + public String getBasicAuthHeader() { + String header = basicAuthHeader; + if (header == null) { + String s = principal + ':' + password; + header = "Basic " + Base64.getEncoder().encodeToString(s.getBytes(charset)); + basicAuthHeader = header; + } + return header; + } + @Override public String toString() { return "Realm{" + diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 66d2b73b3..c6dcd34f2 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -32,7 +32,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.MessageDigest; -import java.util.Base64; import java.util.List; import java.util.Set; @@ -87,12 +86,7 @@ private AuthenticatorUtils() { } private static @Nullable String computeBasicAuthentication(@Nullable Realm realm) { - return realm != null ? computeBasicAuthentication(realm.getPrincipal(), realm.getPassword(), realm.getCharset()) : null; - } - - private static String computeBasicAuthentication(@Nullable String principal, @Nullable String password, Charset charset) { - String s = principal + ':' + password; - return "Basic " + Base64.getEncoder().encodeToString(s.getBytes(charset)); + return realm != null ? realm.getBasicAuthHeader() : null; } public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean omitQuery) { diff --git a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java index c98f19483..c51cbb0d2 100644 --- a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java @@ -40,12 +40,30 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class AuthenticatorUtilsTest { + @Test + void preemptiveBasicAuthorizationHeaderIsMemoizedOnRealm() { + Request request = new RequestBuilder("GET") + .setUrl("http://example.com/api/users") + .build(); + Realm realm = new Realm.Builder("user", "pass") + .setScheme(Realm.AuthScheme.BASIC) + .setUsePreemptiveAuth(true) + .build(); + + String first = AuthenticatorUtils.perRequestAuthorizationHeader(request, realm); + String second = AuthenticatorUtils.perRequestAuthorizationHeader(request, realm); + + assertEquals("Basic dXNlcjpwYXNz", first); + assertSame(first, second); + } + @Test void computeBodyHashEmptyBody() throws Exception { Request request = new RequestBuilder("GET") From f1672154c6bc84f45ca95f099cfac8739f925739 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:33:46 +0200 Subject: [PATCH 2/2] Document Basic auth cache API Document the lazy cache semantics of Realm.getBasicAuthHeader(), including its intentional public API status and benign duplicate computation during concurrent first access. Extend the regression test to verify that request and proxy authentication reuse the same cached header instance. Codex on behalf of Pavel Ptashyts Co-Authored-By: Codex --- client/src/main/java/org/asynchttpclient/Realm.java | 4 +++- .../java/org/asynchttpclient/util/AuthenticatorUtilsTest.java | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index 7b2951776..b41203dd1 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -248,9 +248,11 @@ public int getMaxIterationCount() { } /** - * Returns the HTTP Basic authorization header for this immutable realm. + * Returns the lazily computed and cached HTTP Basic authorization header for this immutable realm. + * Concurrent first calls may compute the same value more than once. * * @return the Basic authorization header + * @since 3.0.12 */ public String getBasicAuthHeader() { String header = basicAuthHeader; diff --git a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java index c51cbb0d2..eea6cd5bc 100644 --- a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java @@ -59,9 +59,11 @@ void preemptiveBasicAuthorizationHeaderIsMemoizedOnRealm() { String first = AuthenticatorUtils.perRequestAuthorizationHeader(request, realm); String second = AuthenticatorUtils.perRequestAuthorizationHeader(request, realm); + String proxy = AuthenticatorUtils.perRequestProxyAuthorizationHeader(request, realm); assertEquals("Basic dXNlcjpwYXNz", first); assertSame(first, second); + assertSame(first, proxy); } @Test