@@ -3811,8 +3811,34 @@ void testSelectedIpsDelayedAddressResolution() {
38113811
38123812@ Test
38133813void testHttpAuthentication () {
3814- AtomicInteger requestCount =new AtomicInteger (0 );
38153814AtomicBoolean authHeaderAdded =new AtomicBoolean (false );
3815+ doTestHttpAuthentication (httpClient ->httpClient .httpAuthentication (
3816+ (req ,res ) ->res .status ().equals (HttpResponseStatus .UNAUTHORIZED ),
3817+ (req ,addr ) -> {
3818+ authHeaderAdded .set (true );
3819+ req .header (HttpHeaderNames .AUTHORIZATION ,"Bearer test-token" );
3820+ }));
3821+ assertThat (authHeaderAdded .get ()).isTrue ();
3822+ }
3823+
3824+ @ Test
3825+ void testHttpAuthenticationWithMonoAuthenticator () {
3826+ AtomicInteger authCallCount =new AtomicInteger (0 );
3827+ doTestHttpAuthentication (httpClient ->httpClient .httpAuthenticationWhen (
3828+ (req ,res ) ->res .status ().equals (HttpResponseStatus .UNAUTHORIZED ),
3829+ (req ,addr ) -> {
3830+ int callNum =authCallCount .incrementAndGet ();
3831+ // Simulate async token generation
3832+ return Mono .delay (Duration .ofMillis (100 ))
3833+ .then (Mono .fromRunnable (() ->req .header (HttpHeaderNames .AUTHORIZATION ,
3834+ "Bearer test-token-" +callNum )));
3835+ }
3836+ ));
3837+ assertThat (authCallCount .get ()).isEqualTo (1 );
3838+ }
3839+
3840+ private void doTestHttpAuthentication (Function <HttpClient ,HttpClient >httpClientCustomizer ) {
3841+ AtomicInteger requestCount =new AtomicInteger (0 );
38163842AtomicReference <HttpClientRequest >capturedRequest =new AtomicReference <>();
38173843
38183844disposableServer =
@@ -3829,22 +3855,14 @@ void testHttpAuthentication() {
38293855 }
38303856else {
38313857// Second request should have auth header
3832- assertThat (authHeader ).isEqualTo ("Bearer test-token" );
3858+ assertThat (authHeader ).startsWith ("Bearer test-token" );
38333859return res .status (HttpResponseStatus .OK )
38343860 .sendString (Mono .just ("Authenticated!" ));
38353861 }
38363862 })
38373863 .bindNow ();
38383864
3839- HttpClient client =
3840- HttpClient .create ()
3841- .port (disposableServer .port ())
3842- .httpAuthentication (
3843- (req ,res ) ->res .status ().equals (HttpResponseStatus .UNAUTHORIZED ),
3844- (req ,addr ) -> {
3845- authHeaderAdded .set (true );
3846- req .header (HttpHeaderNames .AUTHORIZATION ,"Bearer test-token" );
3847- });
3865+ HttpClient client =httpClientCustomizer .apply (HttpClient .create ().port (disposableServer .port ()));
38483866
38493867String response =client .doAfterRequest ((req ,conn ) ->capturedRequest .set (req ))
38503868 .get ()
@@ -3856,7 +3874,6 @@ void testHttpAuthentication() {
38563874
38573875assertThat (response ).isEqualTo ("Authenticated!" );
38583876assertThat (requestCount .get ()).isEqualTo (2 );
3859- assertThat (authHeaderAdded .get ()).isTrue ();
38603877assertThat (capturedRequest .get ()).isNotNull ();
38613878assertThat (capturedRequest .get ().authenticationRetryCount ()).isEqualTo (1 );
38623879}
@@ -3906,61 +3923,6 @@ void testHttpAuthenticationNoRetryWhenPredicateDoesNotMatch() {
39063923assertThat (capturedRequest .get ().authenticationRetryCount ()).isEqualTo (0 );
39073924}
39083925
3909- @ Test
3910- void testHttpAuthenticationWithMonoAuthenticator () {
3911- AtomicInteger requestCount =new AtomicInteger (0 );
3912- AtomicInteger authCallCount =new AtomicInteger (0 );
3913- AtomicReference <HttpClientRequest >capturedRequest =new AtomicReference <>();
3914-
3915- disposableServer =
3916- HttpServer .create ()
3917- .port (0 )
3918- .handle ((req ,res ) -> {
3919- int count =requestCount .incrementAndGet ();
3920- String authHeader =req .requestHeaders ().get (HttpHeaderNames .AUTHORIZATION );
3921-
3922- if (count ==1 ) {
3923- assertThat (authHeader ).isNull ();
3924- return res .status (HttpResponseStatus .UNAUTHORIZED ).send ();
3925- }
3926- else {
3927- assertThat (authHeader ).startsWith ("Bearer async-token-" );
3928- return res .status (HttpResponseStatus .OK )
3929- .sendString (Mono .just ("Success" ));
3930- }
3931- })
3932- .bindNow ();
3933-
3934- HttpClient client =
3935- HttpClient .create ()
3936- .port (disposableServer .port ())
3937- .httpAuthenticationWhen (
3938- (req ,res ) ->res .status ().equals (HttpResponseStatus .UNAUTHORIZED ),
3939- (req ,addr ) -> {
3940- int callNum =authCallCount .incrementAndGet ();
3941- // Simulate async token generation
3942- return Mono .delay (Duration .ofMillis (100 ))
3943- .then (Mono .fromRunnable (() ->
3944- req .header (HttpHeaderNames .AUTHORIZATION ,
3945- "Bearer async-token-" +callNum )));
3946- }
3947- );
3948-
3949- String response =client .doAfterRequest ((req ,conn ) ->capturedRequest .set (req ))
3950- .get ()
3951- .uri ("/api/resource" )
3952- .responseContent ()
3953- .aggregate ()
3954- .asString ()
3955- .block (Duration .ofSeconds (5 ));
3956-
3957- assertThat (response ).isEqualTo ("Success" );
3958- assertThat (requestCount .get ()).isEqualTo (2 );
3959- assertThat (authCallCount .get ()).isEqualTo (1 );
3960- assertThat (capturedRequest .get ()).isNotNull ();
3961- assertThat (capturedRequest .get ().authenticationRetryCount ()).isEqualTo (1 );
3962- }
3963-
39643926@ Test
39653927void testHttpAuthenticationMultipleRequests () {
39663928AtomicInteger requestCount =new AtomicInteger (0 );
@@ -4109,179 +4071,93 @@ void testHttpAuthenticationMaxRetries() {
41094071}
41104072
41114073@ Test
4112- void testHttpAuthenticationRetriesResetPerRequestHttp1 () {
4113- AtomicInteger requestCount =new AtomicInteger (0 );
4114- AtomicInteger authenticatorCallCount =new AtomicInteger (0 );
4115- Set <ChannelId >channelIds =ConcurrentHashMap .newKeySet ();
4116-
4117- disposableServer =
4118- HttpServer .create ()
4119- .port (0 )
4120- .wiretap (true )
4121- .handle ((req ,res ) -> {
4122- int count =requestCount .incrementAndGet ();
4123- String authHeader =req .requestHeaders ().get (HttpHeaderNames .AUTHORIZATION );
4124-
4125- // Track channel ID to verify connection reuse
4126- req .withConnection (conn ->channelIds .add (conn .channel ().id ()));
4127-
4128- // Always return 401 on first attempt (no auth header)
4129- if (authHeader ==null || !authHeader .equals ("Bearer token" )) {
4130- return res .status (HttpResponseStatus .UNAUTHORIZED ).send ();
4131- }
4132- else {
4133- return res .status (HttpResponseStatus .OK )
4134- .sendString (Mono .just ("OK-" +count ));
4135- }
4136- })
4137- .bindNow ();
4138-
4139- ConnectionProvider provider =ConnectionProvider .create ("test" ,1 );
4140-
4141- try {
4142- HttpClient client =
4143- HttpClient .create (provider )
4144- .port (disposableServer .port ())
4145- .protocol (HttpProtocol .HTTP11 )
4146- .httpAuthenticationWhen (
4147- (req ,res ) ->res .status ().equals (HttpResponseStatus .UNAUTHORIZED ),
4148- (req ,addr ) -> {
4149- authenticatorCallCount .incrementAndGet ();
4150- req .header (HttpHeaderNames .AUTHORIZATION ,"Bearer token" );
4151- return Mono .empty ();
4152- }
4153- );
4154-
4155- // First request: 401 -> retry with auth -> 200
4156- String response1 =client .get ()
4157- .uri ("/api/1" )
4158- .responseContent ()
4159- .aggregate ()
4160- .asString ()
4161- .block (Duration .ofSeconds (5 ));
4162- assertThat (response1 ).contains ("OK" );
4163-
4164- // Second request using same connection from pool: should reset authenticationRetries
4165- // Should also trigger: 401 -> retry with auth -> 200
4166- String response2 =client .get ()
4167- .uri ("/api/2" )
4168- .responseContent ()
4169- .aggregate ()
4170- .asString ()
4171- .block (Duration .ofSeconds (5 ));
4172- assertThat (response2 ).contains ("OK" );
4173-
4174- // Third request: same pattern
4175- String response3 =client .get ()
4176- .uri ("/api/3" )
4177- .responseContent ()
4178- .aggregate ()
4179- .asString ()
4180- .block (Duration .ofSeconds (5 ));
4181- assertThat (response3 ).contains ("OK" );
4182-
4183- // Verify:
4184- // - Each request triggered auth retry (3 requests × 2 attempts = 6 total server requests)
4185- assertThat (requestCount .get ()).isEqualTo (6 );
4186- // - Authenticator was called 3 times (once per request)
4187- assertThat (authenticatorCallCount .get ()).isEqualTo (3 );
4188- // - Connection was reused (HTTP/1.1 keep-alive with pool size 1)
4189- // All requests should use the same channel
4190- assertThat (channelIds ).hasSize (1 );
4191- }
4192- finally {
4193- provider .disposeLater ().block (Duration .ofSeconds (5 ));
4194- }
4074+ void testHttpAuthenticationRetriesResetPerRequestHttp1 ()throws Exception {
4075+ doTestHttpAuthenticationRetriesResetPerRequest (false );
41954076}
41964077
41974078@ Test
41984079void testHttpAuthenticationRetriesResetPerRequestHttp2 ()throws Exception {
4080+ doTestHttpAuthenticationRetriesResetPerRequest (true );
4081+ }
4082+
4083+ private void doTestHttpAuthenticationRetriesResetPerRequest (boolean isHttp2 )throws Exception {
41994084AtomicInteger requestCount =new AtomicInteger (0 );
42004085AtomicInteger authenticatorCallCount =new AtomicInteger (0 );
4201- Set <ChannelId >parentChannelIds =ConcurrentHashMap .newKeySet ();
4086+ Set <ChannelId >channelIds =ConcurrentHashMap .newKeySet ();
42024087
42034088SslContext sslServer =SslContextBuilder .forServer (ssc .toTempCertChainPem (),ssc .toTempPrivateKeyPem ()).build ();
42044089SslContext sslClient =SslContextBuilder .forClient ().trustManager (InsecureTrustManagerFactory .INSTANCE ).build ();
42054090
42064091disposableServer =
42074092HttpServer .create ()
42084093 .port (0 )
4209- .protocol (HttpProtocol .H2 )
4210- .secure (spec ->spec .sslContext (sslServer ))
42114094 .wiretap (true )
4095+ .protocol (isHttp2 ?HttpProtocol .H2 :HttpProtocol .HTTP11 )
4096+ .secure (spec ->spec .sslContext (sslServer ))
42124097 .handle ((req ,res ) -> {
42134098int count =requestCount .incrementAndGet ();
42144099String authHeader =req .requestHeaders ().get (HttpHeaderNames .AUTHORIZATION );
42154100
4101+ // Track channel ID to verify connection reuse
42164102// Track parent channel ID (HTTP/2 connection) to verify reuse
4217- req .withConnection (conn -> {
4218- if (conn .channel ().parent () !=null ) {
4219- parentChannelIds .add (conn .channel ().parent ().id ());
4220- }
4221- });
4103+ req .withConnection (conn ->
4104+ channelIds .add (isHttp2 ?conn .channel ().parent ().id () :conn .channel ().id ()));
42224105
42234106// Always return 401 on first attempt (no auth header)
4224- if (authHeader ==null || !authHeader .equals ("Bearerh2- token" )) {
4107+ if (authHeader ==null || !authHeader .equals ("Bearer token" )) {
42254108return res .status (HttpResponseStatus .UNAUTHORIZED ).send ();
42264109 }
42274110else {
42284111return res .status (HttpResponseStatus .OK )
4229- .sendString (Mono .just ("OK-H2- " +count ));
4112+ .sendString (Mono .just ("OK-" +count ));
42304113 }
42314114 })
42324115 .bindNow ();
42334116
4234- ConnectionProvider provider =ConnectionProvider .create ("test-h2 " ,1 );
4117+ ConnectionProvider provider =ConnectionProvider .create ("doTestHttpAuthenticationRetriesResetPerRequest " ,1 );
42354118
42364119try {
42374120HttpClient client =
42384121HttpClient .create (provider )
42394122 .port (disposableServer .port ())
4240- .protocol (HttpProtocol .H2 )
4123+ .protocol (isHttp2 ? HttpProtocol .H2 : HttpProtocol . HTTP11 )
42414124 .secure (spec ->spec .sslContext (sslClient ))
42424125 .httpAuthenticationWhen (
42434126 (req ,res ) ->res .status ().equals (HttpResponseStatus .UNAUTHORIZED ),
42444127 (req ,addr ) -> {
42454128authenticatorCallCount .incrementAndGet ();
4246- req .header (HttpHeaderNames .AUTHORIZATION ,"Bearerh2- token" );
4129+ req .header (HttpHeaderNames .AUTHORIZATION ,"Bearer token" );
42474130return Mono .empty ();
42484131 }
42494132 );
42504133
42514134// First request: 401 -> retry with auth -> 200
4252- String response1 =client .get ()
4253- .uri ("/api/1" )
4254- .responseContent ()
4255- .aggregate ()
4256- .asString ()
4257- .block (Duration .ofSeconds (5 ));
4258- assertThat (response1 ).contains ("OK-H2" );
4259-
4135+ // Second request using same connection from pool: should reset authenticationRetries
4136+ // Should also trigger: 401 -> retry with auth -> 200
42604137// Second request on same HTTP/2 connection (new stream): should reset authenticationRetries
4261- String response2 =client .get ()
4262- .uri ("/api/2" )
4263- .responseContent ()
4264- .aggregate ()
4265- .asString ()
4266- .block (Duration .ofSeconds (5 ));
4267- assertThat (response2 ).contains ("OK-H2" );
4268-
42694138// Third request: same pattern
4270- String response3 =client .get ()
4271- .uri ("/api/3" )
4272- .responseContent ()
4273- .aggregate ()
4274- .asString ()
4275- .block (Duration .ofSeconds (5 ));
4276- assertThat (response3 ).contains ("OK-H2" );
4139+ Flux .range (1 ,3 )
4140+ .concatMap (i ->
4141+ client .get ()
4142+ .uri ("/api/" +i )
4143+ .responseContent ()
4144+ .aggregate ()
4145+ .asString ())
4146+ .collectList ()
4147+ .as (StepVerifier ::create )
4148+ .assertNext (list ->assertThat (list ).allMatch (s ->s .contains ("OK" )))
4149+ .expectComplete ()
4150+ .verify (Duration .ofSeconds (5 ));
42774151
42784152// Verify:
4279- // - Each request triggered auth retry (3 requests × 2 attempts = 6 total)
4153+ // - Each request triggered auth retry (3 requests × 2 attempts = 6 total server requests )
42804154assertThat (requestCount .get ()).isEqualTo (6 );
42814155// - Authenticator was called 3 times (once per request)
42824156assertThat (authenticatorCallCount .get ()).isEqualTo (3 );
4157+ // - Connection was reused (HTTP/1.1 keep-alive with pool size 1)
4158+ // All requests should use the same channel
42834159// - Same HTTP/2 connection was reused for all streams
4284- assertThat (parentChannelIds ).hasSize (1 );
4160+ assertThat (channelIds ).hasSize (1 );
42854161}
42864162finally {
42874163provider .disposeLater ().block (Duration .ofSeconds (5 ));