You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2021/09/27 17:14:01 UTC

[httpcomponents-client] branch master updated (cc12402 -> 54133d6)

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git.


 discard cc12402  Corrected resolution of the target host in DefaultUserTokenHandler
 discard d64cf18  RFC 7230: treat presence of a userinfo component in request URI as an HTTP protocol violation
 discard e843aa2  AuthCache conformance to RFC 7617
     new b31585e  Always bind the exchange ID to the execution context
     new d3ca96c  AuthCache conformance to RFC 7617
     new cab35d2  RFC 7230: treat presence of a userinfo component in request URI as an HTTP protocol violation
     new 54133d6  Corrected resolution of the target host in DefaultUserTokenHandler

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (cc12402)
            \
             N -- N -- N   refs/heads/master (54133d6)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../client5/http/impl/cache/AsyncCachingExec.java  |  1 +
 .../hc/client5/http/impl/cache/CachingExec.java    |  1 +
 .../testing/sync/TestClientAuthentication.java     | 15 ++++++--------
 .../apache/hc/client5/http/auth/AuthExchange.java  | 16 +++++++++++++++
 .../http/impl/DefaultAuthenticationStrategy.java   |  4 ++--
 .../hc/client5/http/impl/RequestSupport.java       | 10 +++++----
 .../client5/http/impl/async/AsyncProtocolExec.java | 11 ++++++++++
 .../async/InternalAbstractHttpAsyncClient.java     |  2 +-
 .../http/impl/async/MinimalH2AsyncClient.java      |  5 ++++-
 .../http/impl/async/MinimalHttpAsyncClient.java    |  7 +++++--
 .../hc/client5/http/impl/auth/AuthCacheKeeper.java | 14 +++++++++----
 .../http/impl/classic/InternalHttpClient.java      |  1 +
 .../http/impl/classic/MinimalHttpClient.java       |  1 +
 .../hc/client5/http/impl/classic/ProtocolExec.java | 11 ++++++++++
 .../hc/client5/http/impl/TestRequestSupport.java   | 24 ++++++++++------------
 15 files changed, 87 insertions(+), 36 deletions(-)

[httpcomponents-client] 03/04: RFC 7230: treat presence of a userinfo component in request URI as an HTTP protocol violation

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit cab35d2250a66e8c64a693e795b02d455fe67788
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Sep 26 11:49:59 2021 +0200

    RFC 7230: treat presence of a userinfo component in request URI as an HTTP protocol violation
---
 .../AbstractHttpAsyncClientAuthentication.java     | 59 ++-------------
 .../testing/sync/TestClientAuthentication.java     | 62 +---------------
 .../apache/hc/client5/http/impl/AuthSupport.java   | 85 ----------------------
 .../client5/http/impl/async/AsyncProtocolExec.java |  8 +-
 .../hc/client5/http/impl/classic/ProtocolExec.java |  8 +-
 .../hc/client5/http/impl/TestAuthSupport.java      | 52 -------------
 .../http/impl/classic/TestProtocolExec.java        | 18 +----
 7 files changed, 14 insertions(+), 278 deletions(-)

diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
index 5de4511..7a98192 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
@@ -30,6 +30,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
@@ -55,7 +56,6 @@ import org.apache.hc.client5.testing.BasicTestAuthenticator;
 import org.apache.hc.client5.testing.auth.Authenticator;
 import org.apache.hc.core5.function.Decorator;
 import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequestInterceptor;
@@ -63,15 +63,14 @@ import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.URIScheme;
 import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.config.Lookup;
 import org.apache.hc.core5.http.config.Registry;
 import org.apache.hc.core5.http.config.RegistryBuilder;
 import org.apache.hc.core5.http.impl.HttpProcessors;
-import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
-import org.apache.hc.core5.http.protocol.HttpCoreContext;
 import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.http2.impl.H2Processors;
@@ -349,24 +348,6 @@ public abstract class AbstractHttpAsyncClientAuthentication<T extends CloseableH
     }
 
     @Test
-    public void testAuthenticationUserinfoInRequestSuccess() throws Exception {
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = start();
-
-        final HttpClientContext context = HttpClientContext.create();
-        final Future<SimpleHttpResponse> future = httpclient.execute(
-                SimpleRequestBuilder.get()
-                        .setScheme(target.getSchemeName())
-                        .setAuthority(new URIAuthority("test:test", target.getHostName(), target.getPort()))
-                        .setPath("/")
-                        .build(), context, null);
-        final SimpleHttpResponse response = future.get();
-
-        Assert.assertNotNull(response);
-        Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
-    }
-
-    @Test
     public void testAuthenticationUserinfoInRequestFailure() throws Exception {
         server.register("*", AsyncEchoHandler::new);
         final HttpHost target = start();
@@ -374,41 +355,11 @@ public abstract class AbstractHttpAsyncClientAuthentication<T extends CloseableH
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = httpclient.execute(SimpleRequestBuilder.get()
                         .setScheme(target.getSchemeName())
-                        .setAuthority(new URIAuthority("test:all-worng", target.getHostName(), target.getPort()))
-                        .setPath("/")
-                        .build(), context, null);
-        final SimpleHttpResponse response = future.get();
-
-        Assert.assertNotNull(response);
-        Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
-    }
-
-    @Test
-    public void testAuthenticationUserinfoInRedirectSuccess() throws Exception {
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = start();
-        server.register("/thatway", () -> new AbstractSimpleServerExchangeHandler() {
-
-            @Override
-            protected SimpleHttpResponse handle(
-                    final SimpleHttpRequest request, final HttpCoreContext context) throws HttpException {
-                final SimpleHttpResponse response = new SimpleHttpResponse(HttpStatus.SC_MOVED_PERMANENTLY);
-                response.addHeader(new BasicHeader("Location", target.getSchemeName() + "://test:test@" + target.toHostString() + "/"));
-                return response;
-            }
-        });
-
-        final HttpClientContext context = HttpClientContext.create();
-        final Future<SimpleHttpResponse> future = httpclient.execute(
-                SimpleRequestBuilder.get()
-                        .setScheme(target.getSchemeName())
                         .setAuthority(new URIAuthority("test:test", target.getHostName(), target.getPort()))
-                        .setPath("/thatway")
+                        .setPath("/")
                         .build(), context, null);
-        final SimpleHttpResponse response = future.get();
-
-        Assert.assertNotNull(response);
-        Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+        final ExecutionException exception = Assert.assertThrows(ExecutionException.class, () -> future.get());
+        MatcherAssert.assertThat(exception.getCause(), CoreMatchers.instanceOf(ProtocolException.class));
     }
 
     @Test
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
index 0af41e1..80c2834 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
@@ -28,7 +28,6 @@ package org.apache.hc.client5.testing.sync;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
@@ -37,6 +36,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
 
+import org.apache.hc.client5.http.ClientProtocolException;
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
@@ -61,7 +61,6 @@ import org.apache.hc.client5.testing.classic.AuthenticatingDecorator;
 import org.apache.hc.client5.testing.classic.EchoHandler;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
-import org.apache.hc.core5.http.EndpointDetails;
 import org.apache.hc.core5.http.HeaderElements;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
@@ -76,9 +75,7 @@ import org.apache.hc.core5.http.io.HttpRequestHandler;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.InputStreamEntity;
 import org.apache.hc.core5.http.io.entity.StringEntity;
-import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.http.protocol.HttpCoreContext;
 import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.net.URIAuthority;
 import org.hamcrest.CoreMatchers;
@@ -434,66 +431,13 @@ public class TestClientAuthentication extends LocalServerTestBase {
     }
 
     @Test
-    public void testAuthenticationUserinfoInRequestSuccess() throws Exception {
+    public void testAuthenticationUserinfoInRequest() throws Exception {
         this.server.registerHandler("*", new EchoHandler());
         final HttpHost target = start();
         final HttpGet httpget = new HttpGet("http://test:test@" +  target.toHostString() + "/");
 
         final HttpClientContext context = HttpClientContext.create();
-        try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) {
-            final HttpEntity entity = response.getEntity();
-            Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
-            Assert.assertNotNull(entity);
-            EntityUtils.consume(entity);
-        }
-    }
-
-    @Test
-    public void testAuthenticationUserinfoInRequestFailure() throws Exception {
-        this.server.registerHandler("*", new EchoHandler());
-        final HttpHost target = start();
-        final HttpGet httpget = new HttpGet("http://test:all-wrong@" +  target.toHostString() + "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) {
-            final HttpEntity entity = response.getEntity();
-            Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
-            Assert.assertNotNull(entity);
-            EntityUtils.consume(entity);
-        }
-    }
-
-    @Test
-    public void testAuthenticationUserinfoInRedirectSuccess() throws Exception {
-        this.server.registerHandler("/*", new EchoHandler());
-        this.server.registerHandler("/thatway", (request, response, context) -> {
-            final EndpointDetails endpoint = (EndpointDetails) context.getAttribute(HttpCoreContext.CONNECTION_ENDPOINT);
-            final InetSocketAddress socketAddress = (InetSocketAddress) endpoint.getLocalAddress();
-            final int port = socketAddress.getPort();
-            response.setCode(HttpStatus.SC_MOVED_PERMANENTLY);
-            response.addHeader(new BasicHeader("Location", "http://test:test@localhost:" + port + "/secure"));
-        });
-
-        final HttpHost target = start(new BasicTestAuthenticator("test:test", "test realm") {
-
-            @Override
-            public boolean authenticate(final URIAuthority authority, final String requestUri, final String credentials) {
-                if (requestUri.equals("/secure") || requestUri.startsWith("/secure/")) {
-                    return super.authenticate(authority, requestUri, credentials);
-                }
-                return true;
-            }
-        });
-
-        final HttpGet httpget = new HttpGet("/thatway");
-        final HttpClientContext context = HttpClientContext.create();
-
-        try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) {
-            final HttpEntity entity = response.getEntity();
-            Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
-            Assert.assertNotNull(entity);
-            EntityUtils.consume(entity);
-        }
+        Assert.assertThrows(ClientProtocolException.class, () -> this.httpclient.execute(target, httpget, context));
     }
 
     @Test
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/AuthSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/AuthSupport.java
deleted file mode 100644
index 8de956f..0000000
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/AuthSupport.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-
-package org.apache.hc.client5.http.impl;
-
-import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.auth.StandardAuthScheme;
-import org.apache.hc.client5.http.auth.AuthScope;
-import org.apache.hc.client5.http.auth.CredentialsStore;
-import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
-import org.apache.hc.core5.annotation.Internal;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.net.URIAuthority;
-import org.apache.hc.core5.util.Args;
-
-/**
- * Authentication support methods.
- *
- * @since 5.0
- */
-@Internal
-public class AuthSupport {
-
-    public static void extractFromAuthority(
-            final String scheme,
-            final URIAuthority authority,
-            final CredentialsStore credentialsStore) {
-        Args.notNull(credentialsStore, "Credentials store");
-        if (authority == null) {
-            return;
-        }
-        final String userInfo = authority.getUserInfo();
-        if (userInfo == null) {
-            return;
-        }
-        final int atColon = userInfo.indexOf(':');
-        final String userName = atColon >= 0 ? userInfo.substring(0, atColon) : userInfo;
-        final char[] password = atColon >= 0 ? userInfo.substring(atColon + 1).toCharArray() : null;
-
-        credentialsStore.setCredentials(
-                new AuthScope(scheme, authority.getHostName(), authority.getPort(), null, StandardAuthScheme.BASIC),
-                new UsernamePasswordCredentials(userName, password));
-    }
-
-    public static HttpHost resolveAuthTarget(final HttpRequest request, final HttpRoute route) {
-        Args.notNull(request, "Request");
-        Args.notNull(route, "Route");
-        final URIAuthority authority = request.getAuthority();
-        final String scheme = request.getScheme();
-        final HttpHost target = authority != null ? new HttpHost(scheme, authority) : route.getTargetHost();
-        if (target.getPort() < 0) {
-            return new HttpHost(
-                    target.getSchemeName(),
-                    target.getHostName(),
-                    route.getTargetHost().getPort());
-        }
-        return target;
-    }
-
-}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
index 7e28534..c4d11b4 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
@@ -39,10 +39,7 @@ import org.apache.hc.client5.http.async.AsyncExecChainHandler;
 import org.apache.hc.client5.http.async.AsyncExecRuntime;
 import org.apache.hc.client5.http.auth.AuthExchange;
 import org.apache.hc.client5.http.auth.ChallengeType;
-import org.apache.hc.client5.http.auth.CredentialsProvider;
-import org.apache.hc.client5.http.auth.CredentialsStore;
 import org.apache.hc.client5.http.config.RequestConfig;
-import org.apache.hc.client5.http.impl.AuthSupport;
 import org.apache.hc.client5.http.impl.RequestSupport;
 import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
 import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
@@ -143,9 +140,8 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
         }
 
         final URIAuthority authority = request.getAuthority();
-        final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
-        if (credsProvider instanceof CredentialsStore) {
-            AuthSupport.extractFromAuthority(request.getScheme(), authority, (CredentialsStore) credsProvider);
+        if (authority.getUserInfo() != null) {
+            throw new ProtocolException("Request URI authority contains deprecated userinfo component");
         }
 
         final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority());
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
index 95f9545..37e7cc5 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
@@ -35,13 +35,10 @@ import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.auth.AuthExchange;
 import org.apache.hc.client5.http.auth.ChallengeType;
-import org.apache.hc.client5.http.auth.CredentialsProvider;
-import org.apache.hc.client5.http.auth.CredentialsStore;
 import org.apache.hc.client5.http.classic.ExecChain;
 import org.apache.hc.client5.http.classic.ExecChainHandler;
 import org.apache.hc.client5.http.classic.ExecRuntime;
 import org.apache.hc.client5.http.config.RequestConfig;
-import org.apache.hc.client5.http.impl.AuthSupport;
 import org.apache.hc.client5.http.impl.RequestSupport;
 import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
 import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
@@ -146,9 +143,8 @@ public final class ProtocolExec implements ExecChainHandler {
             }
 
             final URIAuthority authority = request.getAuthority();
-            final CredentialsProvider credsProvider = context.getCredentialsProvider();
-            if (credsProvider instanceof CredentialsStore) {
-                AuthSupport.extractFromAuthority(request.getScheme(), authority, (CredentialsStore) credsProvider);
+            if (authority.getUserInfo() != null) {
+                throw new ProtocolException("Request URI authority contains deprecated userinfo component");
             }
 
             final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority());
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestAuthSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestAuthSupport.java
deleted file mode 100644
index 1101050..0000000
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestAuthSupport.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl;
-
-import org.apache.hc.client5.http.auth.AuthScope;
-import org.apache.hc.client5.http.auth.Credentials;
-import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
-import org.apache.hc.core5.net.URIAuthority;
-import org.junit.Assert;
-import org.junit.Test;
-
-/**
- * Simple tests for {@link AuthSupport}.
- */
-public class TestAuthSupport {
-
-    @Test
-    public void testExtractFromAuthority() {
-        final URIAuthority uriAuthority = new URIAuthority("testUser", "localhost", 8080);
-        final BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
-
-        AuthSupport.extractFromAuthority("http", uriAuthority, basicCredentialsProvider);
-
-        final Credentials credentials = basicCredentialsProvider.getCredentials(new AuthScope("localhost", 8080), null);
-        Assert.assertEquals("testUser", credentials.getUserPrincipal().getName());
-        Assert.assertNull(credentials.getPassword());
-    }
-}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java
index e0d78dc..b67cad0 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java
@@ -30,7 +30,6 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.URI;
 import java.util.Collections;
 
 import org.apache.hc.client5.http.AuthenticationStrategy;
@@ -38,15 +37,12 @@ import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.auth.AuthExchange;
 import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.ChallengeType;
-import org.apache.hc.client5.http.auth.Credentials;
-import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.classic.ExecChain;
 import org.apache.hc.client5.http.classic.ExecRuntime;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.classic.methods.HttpPost;
 import org.apache.hc.client5.http.entity.EntityBuilder;
-import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
 import org.apache.hc.client5.http.impl.auth.NTLMScheme;
@@ -57,6 +53,7 @@ import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
 import org.apache.hc.core5.http.protocol.HttpProcessor;
 import org.junit.Assert;
@@ -123,20 +120,9 @@ public class TestProtocolExec {
         final HttpRoute route = new HttpRoute(new HttpHost("somehost", 8080));
         final ClassicHttpRequest request = new HttpGet("http://somefella:secret@bar/test");
         final HttpClientContext context = HttpClientContext.create();
-        context.setCredentialsProvider(new BasicCredentialsProvider());
-
-        final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
-        Mockito.when(chain.proceed(
-                Mockito.any(),
-                Mockito.any())).thenReturn(response);
 
         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
-        protocolExec.execute(request, scope, chain);
-        Assert.assertEquals(new URI("http://bar/test"), request.getUri());
-        final CredentialsProvider credentialsProvider = context.getCredentialsProvider();
-        final Credentials creds = credentialsProvider.getCredentials(new AuthScope(null, "bar", -1, null, null), null);
-        Assert.assertNotNull(creds);
-        Assert.assertEquals("somefella", creds.getUserPrincipal().getName());
+        Assert.assertThrows(ProtocolException.class, () -> protocolExec.execute(request, scope, chain));
     }
 
     @Test

[httpcomponents-client] 01/04: Always bind the exchange ID to the execution context

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit b31585e23a237c01af502a3e0960582ece1e7ae2
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Mon Sep 27 14:35:24 2021 +0200

    Always bind the exchange ID to the execution context
---
 .../org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java    | 1 +
 .../java/org/apache/hc/client5/http/impl/cache/CachingExec.java    | 1 +
 .../client5/http/impl/async/InternalAbstractHttpAsyncClient.java   | 2 +-
 .../apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java    | 5 ++++-
 .../apache/hc/client5/http/impl/async/MinimalHttpAsyncClient.java  | 7 +++++--
 .../apache/hc/client5/http/impl/classic/InternalHttpClient.java    | 1 +
 .../org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java | 1 +
 7 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java
index c9001b2..70d7a47 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java
@@ -656,6 +656,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
                 try {
                     final SimpleHttpResponse cacheResponse = generateCachedResponse(request, context, entry, now);
                     final String exchangeId = ExecSupport.getNextExchangeId();
+                    context.setExchangeId(exchangeId);
                     final AsyncExecChain.Scope fork = new AsyncExecChain.Scope(
                             exchangeId,
                             scope.route,
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java
index 801f5a7..3f756fe 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java
@@ -278,6 +278,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
                         && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
                     LOG.debug("Serving stale with asynchronous revalidation");
                     final String exchangeId = ExecSupport.getNextExchangeId();
+                    context.setExchangeId(exchangeId);
                     final ExecChain.Scope fork = new ExecChain.Scope(
                             exchangeId,
                             scope.route,
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalAbstractHttpAsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalAbstractHttpAsyncClient.java
index c4fd294..9496a32 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalAbstractHttpAsyncClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalAbstractHttpAsyncClient.java
@@ -188,12 +188,12 @@ abstract class InternalAbstractHttpAsyncClient extends AbstractHttpAsyncClientBa
                         httpHost != null ? httpHost : RoutingSupport.determineHost(request),
                         clientContext);
                 final String exchangeId = ExecSupport.getNextExchangeId();
+                clientContext.setExchangeId(exchangeId);
                 if (LOG.isDebugEnabled()) {
                     LOG.debug("{} preparing request execution", exchangeId);
                 }
                 final AsyncExecRuntime execRuntime = createAsyncExecRuntime(pushHandlerFactory);
 
-                clientContext.setExchangeId(exchangeId);
                 setupContext(clientContext);
 
                 final AsyncExecChain.Scheduler scheduler = this::executeScheduled;
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java
index a88b26e..a4bbee6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java
@@ -213,7 +213,10 @@ public final class MinimalH2AsyncClient extends AbstractMinimalHttpAsyncClientBa
                         };
                         if (LOG.isDebugEnabled()) {
                             final String exchangeId = ExecSupport.getNextExchangeId();
-                            LOG.debug("{} executing message exchange {}", exchangeId, ConnPoolSupport.getId(session));
+                            clientContext.setExchangeId(exchangeId);
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("{} executing message exchange {}", exchangeId, ConnPoolSupport.getId(session));
+                            }
                             session.enqueue(
                                     new RequestExecutionCommand(
                                             new LoggingAsyncClientExchangeHandler(LOG, exchangeId, internalExchangeHandler),
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalHttpAsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalHttpAsyncClient.java
index 0363c86..6a7bb4e 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalHttpAsyncClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalHttpAsyncClient.java
@@ -136,6 +136,7 @@ public final class MinimalHttpAsyncClient extends AbstractMinimalHttpAsyncClient
         final HttpRoute route = new HttpRoute(RoutingSupport.normalize(host, schemePortResolver));
         final ComplexFuture<AsyncConnectionEndpoint> resultFuture = new ComplexFuture<>(callback);
         final String exchangeId = ExecSupport.getNextExchangeId();
+        clientContext.setExchangeId(exchangeId);
         final Future<AsyncConnectionEndpoint> leaseFuture = manager.lease(
                 exchangeId,
                 route,
@@ -445,16 +446,18 @@ public final class MinimalHttpAsyncClient extends AbstractMinimalHttpAsyncClient
                 final HttpContext context) {
             Asserts.check(!released.get(), "Endpoint has already been released");
 
+            final HttpClientContext clientContext = context != null ? HttpClientContext.adapt(context) : HttpClientContext.create();
             final String exchangeId = ExecSupport.getNextExchangeId();
+            clientContext.setExchangeId(exchangeId);
             if (LOG.isDebugEnabled()) {
                 LOG.debug("{} executing message exchange {}", exchangeId, ConnPoolSupport.getId(connectionEndpoint));
                 connectionEndpoint.execute(
                         exchangeId,
                         new LoggingAsyncClientExchangeHandler(LOG, exchangeId, exchangeHandler),
                         pushHandlerFactory,
-                        context);
+                        clientContext);
             } else {
-                connectionEndpoint.execute(exchangeId, exchangeHandler, context);
+                connectionEndpoint.execute(exchangeId, exchangeHandler, clientContext);
             }
         }
 
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
index da5a188..699784d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
@@ -159,6 +159,7 @@ class InternalHttpClient extends CloseableHttpClient implements Configurable {
                     target != null ? target : RoutingSupport.determineHost(request),
                     localcontext);
             final String exchangeId = ExecSupport.getNextExchangeId();
+            localcontext.setExchangeId(exchangeId);
             if (LOG.isDebugEnabled()) {
                 LOG.debug("{} preparing request execution", exchangeId);
             }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java
index b6f4230..24f8b9d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java
@@ -132,6 +132,7 @@ public class MinimalHttpClient extends CloseableHttpClient {
 
         final HttpRoute route = new HttpRoute(RoutingSupport.normalize(target, schemePortResolver));
         final String exchangeId = ExecSupport.getNextExchangeId();
+        clientContext.setExchangeId(exchangeId);
         final ExecRuntime execRuntime = new InternalExecRuntime(LOG, connManager, requestExecutor,
                 request instanceof CancellableDependency ? (CancellableDependency) request : null);
         try {

[httpcomponents-client] 04/04: Corrected resolution of the target host in DefaultUserTokenHandler

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit 54133d6585a0b4a57bd67b1fcb7070c5dd5ac943
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Sep 26 15:51:23 2021 +0200

    Corrected resolution of the target host in DefaultUserTokenHandler
---
 .../apache/hc/client5/http/UserTokenHandler.java   | 19 +++++++++++
 .../client5/http/impl/DefaultUserTokenHandler.java | 38 ++++++++++++++--------
 .../http/impl/async/HttpAsyncMainClientExec.java   |  2 +-
 .../client5/http/impl/classic/MainClientExec.java  |  2 +-
 4 files changed, 46 insertions(+), 15 deletions(-)

diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/UserTokenHandler.java b/httpclient5/src/main/java/org/apache/hc/client5/http/UserTokenHandler.java
index deb0944..68e2f1d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/UserTokenHandler.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/UserTokenHandler.java
@@ -29,6 +29,7 @@ package org.apache.hc.client5.http;
 
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.protocol.HttpContext;
 
 /**
@@ -60,4 +61,22 @@ public interface UserTokenHandler {
      */
     Object getUserToken(HttpRoute route, HttpContext context);
 
+    /**
+     * The token object returned by this method is expected to uniquely
+     * identify the current user if the context is user specific or to be
+     * {@code null} if it is not.
+     *
+     * @param route HTTP route
+     * @param request HTTP request
+     * @param context the execution context
+     *
+     * @return user token that uniquely identifies the user or
+     * {@code null} if the context is not user specific.
+     *
+     * @since 5.2
+     */
+    default Object getUserToken(HttpRoute route, HttpRequest request, HttpContext context) {
+        return getUserToken(route, context);
+    }
+
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultUserTokenHandler.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultUserTokenHandler.java
index 6570cc4..a41872f 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultUserTokenHandler.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultUserTokenHandler.java
@@ -37,6 +37,8 @@ import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.protocol.HttpContext;
 
 /**
@@ -61,28 +63,38 @@ public class DefaultUserTokenHandler implements UserTokenHandler {
 
     @Override
     public Object getUserToken(final HttpRoute route, final HttpContext context) {
+        return getUserToken(route, null, context);
+    }
+
+    @Override
+    public Object getUserToken(final HttpRoute route, final HttpRequest request, final HttpContext context) {
 
         final HttpClientContext clientContext = HttpClientContext.adapt(context);
 
-        Principal userPrincipal = null;
+        final HttpHost target = request != null ? new HttpHost(request.getScheme(), request.getAuthority()) : route.getTargetHost();
 
-        final AuthExchange targetAuthExchange = clientContext.getAuthExchange(route.getTargetHost());
+        final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target);
         if (targetAuthExchange != null) {
-            userPrincipal = getAuthPrincipal(targetAuthExchange);
-            if (userPrincipal == null && route.getProxyHost() != null) {
-                final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(route.getProxyHost());
-                userPrincipal = getAuthPrincipal(proxyAuthExchange);
+            final Principal authPrincipal = getAuthPrincipal(targetAuthExchange);
+            if (authPrincipal != null) {
+                return authPrincipal;
             }
         }
-
-        if (userPrincipal == null) {
-            final SSLSession sslSession = clientContext.getSSLSession();
-            if (sslSession != null) {
-                userPrincipal = sslSession.getLocalPrincipal();
+        final HttpHost proxy = route.getProxyHost();
+        if (proxy != null) {
+            final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(proxy);
+            if (proxyAuthExchange != null) {
+                final Principal authPrincipal = getAuthPrincipal(proxyAuthExchange);
+                if (authPrincipal != null) {
+                    return authPrincipal;
+                }
             }
         }
-
-        return userPrincipal;
+        final SSLSession sslSession = clientContext.getSSLSession();
+        if (sslSession != null) {
+            return sslSession.getLocalPrincipal();
+        }
+        return null;
     }
 
     private static Principal getAuthPrincipal(final AuthExchange authExchange) {
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java
index 7816f44..c8e4e26 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java
@@ -196,7 +196,7 @@ class HttpAsyncMainClientExec implements AsyncExecChainHandler {
                 final TimeValue keepAliveDuration = keepAliveStrategy.getKeepAliveDuration(response, clientContext);
                 Object userToken = clientContext.getUserToken();
                 if (userToken == null) {
-                    userToken = userTokenHandler.getUserToken(route, clientContext);
+                    userToken = userTokenHandler.getUserToken(route, request, clientContext);
                     clientContext.setAttribute(HttpClientContext.USER_TOKEN, userToken);
                 }
                 execRuntime.markConnectionReusable(userToken, keepAliveDuration);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java
index 2af5b70..3c79f60 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java
@@ -106,7 +106,7 @@ public final class MainClientExec implements ExecChainHandler {
 
             Object userToken = context.getUserToken();
             if (userToken == null) {
-                userToken = userTokenHandler.getUserToken(route, context);
+                userToken = userTokenHandler.getUserToken(route, request, context);
                 context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
             }
 

[httpcomponents-client] 02/04: AuthCache conformance to RFC 7617

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit d3ca96cca1ac05e71771a483ea617c542b1c7dcf
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Sep 26 11:21:13 2021 +0200

    AuthCache conformance to RFC 7617
---
 .../AbstractHttpAsyncClientAuthentication.java     |  78 +++++++++
 .../testing/async/TestH2ClientAuthentication.java  |  12 ++
 .../async/TestHttp1ClientAuthentication.java       |  12 ++
 .../testing/sync/TestClientAuthentication.java     |  74 ++++++++
 .../apache/hc/client5/http/SchemePortResolver.java |  10 ++
 .../org/apache/hc/client5/http/auth/AuthCache.java |  61 +++++++
 .../apache/hc/client5/http/auth/AuthExchange.java  |  16 ++
 .../http/impl/DefaultAuthenticationStrategy.java   |   4 +-
 .../http/impl/DefaultSchemePortResolver.java       |  14 +-
 .../{ProtocolSupport.java => RequestSupport.java}  |  61 +++----
 .../client5/http/impl/async/AsyncConnectExec.java  |   8 +-
 .../client5/http/impl/async/AsyncProtocolExec.java |  36 ++--
 .../hc/client5/http/impl/auth/AuthCacheKeeper.java |  33 ++--
 .../hc/client5/http/impl/auth/BasicAuthCache.java  |  96 +++++++++--
 .../hc/client5/http/impl/classic/ConnectExec.java  |   8 +-
 .../hc/client5/http/impl/classic/ProtocolExec.java |  31 +++-
 .../hc/client5/http/impl/TestProtocolSupport.java  |  55 ------
 .../hc/client5/http/impl/TestRequestSupport.java   |  53 ++++++
 .../client5/http/impl/auth/TestBasicAuthCache.java |   2 +-
 .../http/impl/auth/TestRequestAuthCache.java       | 188 ---------------------
 20 files changed, 527 insertions(+), 325 deletions(-)

diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
index 88f73b2..5de4511 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
@@ -26,14 +26,19 @@
  */
 package org.apache.hc.client5.testing.async;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
 
 import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
 import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
@@ -42,6 +47,7 @@ import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
 import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -52,7 +58,9 @@ import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
 import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.URIScheme;
@@ -64,9 +72,12 @@ import org.apache.hc.core5.http.impl.HttpProcessors;
 import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
 import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.http2.impl.H2Processors;
 import org.apache.hc.core5.net.URIAuthority;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -104,6 +115,10 @@ public abstract class AbstractHttpAsyncClientAuthentication<T extends CloseableH
 
     abstract void setTargetAuthenticationStrategy(AuthenticationStrategy targetAuthStrategy);
 
+    abstract void addResponseInterceptor(HttpResponseInterceptor responseInterceptor);
+
+    abstract void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor);
+
     @Test
     public void testBasicAuthenticationNoCreds() throws Exception {
         server.register("*", AsyncEchoHandler::new);
@@ -271,6 +286,69 @@ public abstract class AbstractHttpAsyncClientAuthentication<T extends CloseableH
     }
 
     @Test
+    public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
+        server.register("*", AsyncEchoHandler::new);
+
+        final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
+        setTargetAuthenticationStrategy(authStrategy);
+        final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
+        addResponseInterceptor((response, entity, context)
+                -> responseQueue.add(BasicResponseBuilder.copy(response).build()));
+
+        final HttpHost target = start();
+
+        final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
+                .add(target, "test", "test".toCharArray())
+                .build();
+
+        final AuthCache authCache = new BasicAuthCache();
+
+        for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
+            final HttpClientContext context = HttpClientContext.create();
+            context.setAuthCache(authCache);
+            context.setCredentialsProvider(credentialsProvider);
+            final Future<SimpleHttpResponse> future = httpclient.execute(SimpleRequestBuilder.get()
+                    .setHttpHost(target)
+                    .setPath(requestPath)
+                    .build(), context, null);
+            final HttpResponse response = future.get();
+            Assert.assertNotNull(response);
+            Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+        }
+
+        // There should be only single auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200)));
+
+        responseQueue.clear();
+        authCache.clear();
+        Mockito.reset(authStrategy);
+
+        for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/"}) {
+            final HttpClientContext context = HttpClientContext.create();
+            context.setCredentialsProvider(credentialsProvider);
+            context.setAuthCache(authCache);
+            final Future<SimpleHttpResponse> future = httpclient.execute(SimpleRequestBuilder.get()
+                    .setHttpHost(target)
+                    .setPath(requestPath)
+                    .build(), context, null);
+            final HttpResponse response = future.get();
+            Assert.assertNotNull(response);
+            Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+        }
+
+        // There should be an auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy, Mockito.times(3)).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 401, 200, 401, 200)));
+    }
+
+    @Test
     public void testAuthenticationUserinfoInRequestSuccess() throws Exception {
         server.register("*", AsyncEchoHandler::new);
         final HttpHost target = start();
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
index 52adf5a..94b6766 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
@@ -38,6 +38,8 @@ import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder;
 import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
 import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
 import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.URIScheme;
 import org.apache.hc.core5.http.config.Lookup;
@@ -93,6 +95,16 @@ public class TestH2ClientAuthentication extends AbstractHttpAsyncClientAuthentic
     }
 
     @Override
+    void addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) {
+        clientBuilder.addResponseInterceptorLast(responseInterceptor);
+    }
+
+    @Override
+    void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) {
+        clientBuilder.addRequestInterceptorLast(requestInterceptor);
+    }
+
+    @Override
     protected CloseableHttpAsyncClient createClient() throws Exception {
         return clientBuilder.build();
     }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
index d8ff129..1459237 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
@@ -51,7 +51,9 @@ import org.apache.hc.client5.testing.SSLTestContexts;
 import org.apache.hc.core5.http.HeaderElements;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
 import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.URIScheme;
@@ -133,6 +135,16 @@ public class TestHttp1ClientAuthentication extends AbstractHttpAsyncClientAuthen
     }
 
     @Override
+    void addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) {
+        clientBuilder.addResponseInterceptorLast(responseInterceptor);
+    }
+
+    @Override
+    void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) {
+        clientBuilder.addRequestInterceptorLast(requestInterceptor);
+    }
+
+    @Override
     protected CloseableHttpAsyncClient createClient() throws Exception {
         return clientBuilder.build();
     }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
index 1e88a46..0af41e1 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
@@ -30,8 +30,12 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
 
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthScheme;
@@ -63,6 +67,7 @@ import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.config.Registry;
 import org.apache.hc.core5.http.config.RegistryBuilder;
@@ -74,7 +79,10 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.net.URIAuthority;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -267,6 +275,9 @@ public class TestClientAuthentication extends LocalServerTestBase {
         this.server.registerHandler("*", new EchoHandler());
         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
         this.clientBuilder.setTargetAuthenticationStrategy(authStrategy);
+        final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
+        this.clientBuilder.addResponseInterceptorLast((response, entity, context)
+                -> responseQueue.add(BasicResponseBuilder.copy(response).build()));
 
         final HttpHost target = start();
 
@@ -286,6 +297,69 @@ public class TestClientAuthentication extends LocalServerTestBase {
         }
 
         Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200, 200)));
+    }
+
+    @Test
+    public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
+        this.server.registerHandler("*", new EchoHandler());
+        final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
+        this.clientBuilder.setTargetAuthenticationStrategy(authStrategy);
+        final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
+        this.clientBuilder.addResponseInterceptorLast((response, entity, context)
+                -> responseQueue.add(BasicResponseBuilder.copy(response).build()));
+
+        final HttpHost target = start();
+
+        final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
+                .add(target, "test", "test".toCharArray())
+                .build();
+
+        final AuthCache authCache = new BasicAuthCache();
+        final HttpClientContext context = HttpClientContext.create();
+        context.setAuthCache(authCache);
+        context.setCredentialsProvider(credentialsProvider);
+
+        for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
+            final HttpGet httpget = new HttpGet(requestPath);
+            try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) {
+                final HttpEntity entity1 = response.getEntity();
+                Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+                Assert.assertNotNull(entity1);
+                EntityUtils.consume(entity1);
+            }
+        }
+
+        // There should be only single auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200)));
+
+        responseQueue.clear();
+        authCache.clear();
+        Mockito.reset(authStrategy);
+
+        for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/", "/buh/a"}) {
+            final HttpGet httpget = new HttpGet(requestPath);
+            try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) {
+                final HttpEntity entity1 = response.getEntity();
+                Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+                Assert.assertNotNull(entity1);
+                EntityUtils.consume(entity1);
+            }
+        }
+
+        // There should be an auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy, Mockito.times(2)).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(200, 401, 200, 200, 401, 200)));
     }
 
     @Test
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
index 26ddb42..f57e019 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
@@ -29,6 +29,7 @@ package org.apache.hc.client5.http;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.net.NamedEndpoint;
 
 /**
  * Strategy for default port resolution for protocol schemes.
@@ -43,4 +44,13 @@ public interface SchemePortResolver {
      */
     int resolve(HttpHost host);
 
+    /**
+     * Returns the actual port for the host based on the protocol scheme.
+     *
+     * @since 5.2
+     */
+    default int resolve(String scheme, NamedEndpoint endpoint) {
+        return resolve(new HttpHost(scheme, endpoint));
+    }
+
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java
index f566b39..e13bde7 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java
@@ -36,12 +36,73 @@ import org.apache.hc.core5.http.HttpHost;
  */
 public interface AuthCache {
 
+    /**
+     * Stores the authentication state with the given authentication scope in the cache.
+     *
+     * @param host the authentication authority.
+     * @param authScheme the cacheable authentication state.
+     */
     void put(HttpHost host, AuthScheme authScheme);
 
+    /**
+     * Returns the authentication state with the given authentication scope from the cache
+     * if available.
+     *
+     * @param host the authentication authority.
+     * @return the authentication state ir {@code null} if not available in the cache.
+     */
     AuthScheme get(HttpHost host);
 
+    /**
+     * Removes the authentication state with the given authentication scope from the cache
+     * if found.
+     *
+     * @param host the authentication authority.
+     */
     void remove(HttpHost host);
 
     void clear();
 
+    /**
+     * Stores the authentication state with the given authentication scope in the cache.
+     *
+     * @param host the authentication authority.
+     * @param pathPrefix the path prefix (the path component up to the last segment separator).
+     *                   Can be {@code null}.
+     * @param authScheme the cacheable authentication state.
+     *
+     * @since 5.2
+     */
+    default void put(HttpHost host, String pathPrefix, AuthScheme authScheme) {
+        put(host, authScheme);
+    }
+
+    /**
+     * Returns the authentication state with the given authentication scope from the cache
+     * if available.
+     * @param host the authentication authority.
+     * @param pathPrefix the path prefix (the path component up to the last segment separator).
+     *                   Can be {@code null}.
+     * @return the authentication state ir {@code null} if not available in the cache.
+     *
+     * @since 5.2
+     */
+    default AuthScheme get(HttpHost host, String pathPrefix) {
+        return get(host);
+    }
+
+    /**
+     * Removes the authentication state with the given authentication scope from the cache
+     * if found.
+     *
+     * @param host the authentication authority.
+     * @param pathPrefix the path prefix (the path component up to the last segment separator).
+     *                   Can be {@code null}.
+     *
+     * @since 5.2
+     */
+    default void remove(HttpHost host, String pathPrefix) {
+        remove(host);
+    }
+
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
index 4709149..2aaf1fb 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
@@ -47,6 +47,7 @@ public class AuthExchange {
     private State state;
     private AuthScheme authScheme;
     private Queue<AuthScheme> authOptions;
+    private String pathPrefix;
 
     public AuthExchange() {
         super();
@@ -57,6 +58,7 @@ public class AuthExchange {
         this.state = State.UNCHALLENGED;
         this.authOptions = null;
         this.authScheme = null;
+        this.pathPrefix = null;
     }
 
     public State getState() {
@@ -82,6 +84,20 @@ public class AuthExchange {
     }
 
     /**
+     * @since 5.2
+     */
+    public String getPathPrefix() {
+        return pathPrefix;
+    }
+
+    /**
+     * @since 5.2
+     */
+    public void setPathPrefix(final String pathPrefix) {
+        this.pathPrefix = pathPrefix;
+    }
+
+    /**
      * Resets the auth state with {@link AuthScheme} and clears auth options.
      *
      * @param authScheme auth scheme. May not be null.
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
index ca0fdd4..a25f563 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
@@ -39,8 +39,8 @@ import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.auth.AuthChallenge;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
-import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.annotation.Contract;
@@ -117,7 +117,7 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy {
                 options.add(authScheme);
             } else {
                 if (LOG.isDebugEnabled()) {
-                    LOG.debug("{}, Challenge for {} authentication scheme not available", exchangeId, schemeName);
+                    LOG.debug("{} Challenge for {} authentication scheme not available", exchangeId, schemeName);
                 }
             }
         }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
index 144ae5f..c65e65d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
@@ -31,6 +31,7 @@ import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.net.NamedEndpoint;
 import org.apache.hc.core5.util.Args;
 
 /**
@@ -46,14 +47,19 @@ public class DefaultSchemePortResolver implements SchemePortResolver {
     @Override
     public int resolve(final HttpHost host) {
         Args.notNull(host, "HTTP host");
-        final int port = host.getPort();
+        return resolve(host.getSchemeName(), host);
+    }
+
+    @Override
+    public int resolve(final String scheme, final NamedEndpoint endpoint) {
+        Args.notNull(endpoint, "Endpoint");
+        final int port = endpoint.getPort();
         if (port > 0) {
             return port;
         }
-        final String name = host.getSchemeName();
-        if (URIScheme.HTTP.same(name)) {
+        if (URIScheme.HTTP.same(scheme)) {
             return 80;
-        } else if (URIScheme.HTTPS.same(name)) {
+        } else if (URIScheme.HTTPS.same(scheme)) {
             return 443;
         } else {
             return -1;
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java
similarity index 51%
rename from httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java
rename to httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java
index 70f820c..50e879f 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java
@@ -26,45 +26,48 @@
  */
 package org.apache.hc.client5.http.impl;
 
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
 import org.apache.hc.core5.annotation.Internal;
 import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.net.URIAuthority;
+import org.apache.hc.core5.net.PercentCodec;
+import org.apache.hc.core5.net.URIBuilder;
 
 /**
- * Protocol support methods.
+ * Protocol support methods. For internal use only.
  *
- * @since 5.1
+ * @since 5.2
  */
 @Internal
-public final class ProtocolSupport {
+public final class RequestSupport {
 
-    public static String getRequestUri(final HttpRequest request) {
-        final URIAuthority authority = request.getAuthority();
-        if (authority != null) {
-            final StringBuilder buf = new StringBuilder();
-            final String scheme = request.getScheme();
-            buf.append(scheme != null ? scheme : URIScheme.HTTP.id);
-            buf.append("://");
-            if (authority.getUserInfo() != null) {
-                buf.append(authority.getUserInfo());
-                buf.append("@");
-            }
-            buf.append(authority.getHostName());
-            if (authority.getPort() != -1) {
-                buf.append(":");
-                buf.append(authority.getPort());
-            }
-            final String path = request.getPath();
-            if (path == null || !path.startsWith("/")) {
-                buf.append("/");
+    public static String extractPathPrefix(final HttpRequest request) {
+        final String path = request.getPath();
+        try {
+            final URIBuilder uriBuilder = new URIBuilder(path);
+            uriBuilder.setFragment(null);
+            uriBuilder.clearParameters();
+            uriBuilder.normalizeSyntax();
+            final List<String> pathSegments = uriBuilder.getPathSegments();
+
+            if (!pathSegments.isEmpty()) {
+                pathSegments.remove(pathSegments.size() - 1);
             }
-            if (path != null) {
-                buf.append(path);
+            if (pathSegments.isEmpty()) {
+                return "/";
+            } else {
+                final StringBuilder buf = new StringBuilder();
+                buf.append('/');
+                for (final String pathSegment : pathSegments) {
+                    PercentCodec.encode(buf, pathSegment, StandardCharsets.US_ASCII);
+                    buf.append('/');
+                }
+                return buf.toString();
             }
-            return buf.toString();
-        } else {
-            return request.getPath();
+        } catch (final URISyntaxException ex) {
+            return path;
         }
     }
 
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
index 4f83962..17d75c6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
@@ -379,7 +379,7 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
         final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange();
 
         if (authCacheKeeper != null) {
-            authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext);
+            authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext);
         }
 
         final HttpRequest connect = new BasicHttpRequest(Method.CONNECT, nextHop, nextHop.toHostString());
@@ -444,9 +444,9 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (proxyAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -455,7 +455,7 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
                         proxyAuthStrategy, proxyAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                 }
 
                 return updated;
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
index 4fe3637..7e28534 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
@@ -43,6 +43,7 @@ import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.CredentialsStore;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.AuthSupport;
+import org.apache.hc.client5.http.impl.RequestSupport;
 import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
 import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -148,23 +149,36 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
         }
 
         final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority());
+        final String pathPrefix = RequestSupport.extractPathPrefix(request);
         final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target);
         final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange();
 
+        if (!targetAuthExchange.isConnectionBased() &&
+                targetAuthExchange.getPathPrefix() != null &&
+                !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) {
+            // force re-authentication if the current path prefix does not match
+            // that of the previous authentication exchange.
+            targetAuthExchange.reset();
+        }
+        if (targetAuthExchange.getPathPrefix() == null) {
+            targetAuthExchange.setPathPrefix(pathPrefix);
+        }
+
         if (authCacheKeeper != null) {
-            authCacheKeeper.loadPreemptively(target, targetAuthExchange, clientContext);
+            authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, clientContext);
             if (proxy != null) {
-                authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext);
+                authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext);
             }
         }
 
         final AtomicBoolean challenged = new AtomicBoolean(false);
-        internalExecute(target, targetAuthExchange, proxyAuthExchange,
+        internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
                 challenged, request, entityProducer, scope, chain, asyncExecCallback);
     }
 
     private void internalExecute(
             final HttpHost target,
+            final String pathPrefix,
             final AuthExchange targetAuthExchange,
             final AuthExchange proxyAuthExchange,
             final AtomicBoolean challenged,
@@ -216,6 +230,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                         proxyAuthExchange,
                         proxy != null ? proxy : target,
                         target,
+                        pathPrefix,
                         response,
                         clientContext)) {
                     challenged.set(true);
@@ -267,7 +282,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                             if (entityProducer != null) {
                                 entityProducer.releaseResources();
                             }
-                            internalExecute(target, targetAuthExchange, proxyAuthExchange,
+                            internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
                                     challenged, request, entityProducer, scope, chain, asyncExecCallback);
                         } catch (final HttpException | IOException ex) {
                             asyncExecCallback.failed(ex);
@@ -298,6 +313,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
             final AuthExchange proxyAuthExchange,
             final HttpHost proxy,
             final HttpHost target,
+            final String pathPrefix,
             final HttpResponse response,
             final HttpClientContext context) {
         final RequestConfig config = context.getRequestConfig();
@@ -307,9 +323,9 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (targetAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context);
                 }
             }
 
@@ -318,9 +334,9 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (proxyAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -329,7 +345,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                         targetAuthStrategy, targetAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
                 }
 
                 return updated;
@@ -339,7 +355,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                         proxyAuthStrategy, proxyAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                 }
 
                 return updated;
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java
index 01f959a..10ce0b6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java
@@ -59,32 +59,39 @@ public final class AuthCacheKeeper {
     }
 
     public void updateOnChallenge(final HttpHost host,
+                                  final String pathPrefix,
                                   final AuthExchange authExchange,
                                   final HttpContext context) {
-        clearCache(host, HttpClientContext.adapt(context));
+        clearCache(host, pathPrefix, HttpClientContext.adapt(context));
     }
 
     public void updateOnNoChallenge(final HttpHost host,
+                                    final String pathPrefix,
                                     final AuthExchange authExchange,
                                     final HttpContext context) {
         if (authExchange.getState() == AuthExchange.State.SUCCESS) {
-            updateCache(host, authExchange.getAuthScheme(), HttpClientContext.adapt(context));
+            updateCache(host, pathPrefix, authExchange.getAuthScheme(), HttpClientContext.adapt(context));
         }
     }
 
     public void updateOnResponse(final HttpHost host,
+                                 final String pathPrefix,
                                  final AuthExchange authExchange,
                                  final HttpContext context) {
         if (authExchange.getState() == AuthExchange.State.FAILURE) {
-            clearCache(host, HttpClientContext.adapt(context));
+            clearCache(host, pathPrefix, HttpClientContext.adapt(context));
         }
     }
 
     public void loadPreemptively(final HttpHost host,
+                                 final String pathPrefix,
                                  final AuthExchange authExchange,
                                  final HttpContext context) {
         if (authExchange.getState() == AuthExchange.State.UNCHALLENGED) {
-            final AuthScheme authScheme = loadFromCache(host, HttpClientContext.adapt(context));
+            AuthScheme authScheme = loadFromCache(host, pathPrefix, HttpClientContext.adapt(context));
+            if (authScheme == null && pathPrefix != null) {
+                authScheme = loadFromCache(host, null, HttpClientContext.adapt(context));
+            }
             if (authScheme != null) {
                 authExchange.select(authScheme);
             }
@@ -92,14 +99,16 @@ public final class AuthCacheKeeper {
     }
 
     private AuthScheme loadFromCache(final HttpHost host,
+                                     final String pathPrefix,
                                      final HttpClientContext clientContext) {
         final AuthCache authCache = clientContext.getAuthCache();
         if (authCache != null) {
-            final AuthScheme authScheme = authCache.get(host);
+            final AuthScheme authScheme = authCache.get(host, pathPrefix);
             if (authScheme != null) {
                 if (LOG.isDebugEnabled()) {
                     final String exchangeId = clientContext.getExchangeId();
-                    LOG.debug("{} Re-using cached '{}' auth scheme for {}", exchangeId, authScheme.getName(), host);
+                    LOG.debug("{} Re-using cached '{}' auth scheme for {}{}", exchangeId, authScheme.getName(), host,
+                            pathPrefix != null ? pathPrefix : "");
                 }
                 return authScheme;
             }
@@ -108,6 +117,7 @@ public final class AuthCacheKeeper {
     }
 
     private void updateCache(final HttpHost host,
+                             final String pathPrefix,
                              final AuthScheme authScheme,
                              final HttpClientContext clientContext) {
         final boolean cacheable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null;
@@ -119,21 +129,24 @@ public final class AuthCacheKeeper {
             }
             if (LOG.isDebugEnabled()) {
                 final String exchangeId = clientContext.getExchangeId();
-                LOG.debug("{} Caching '{}' auth scheme for {}", exchangeId, authScheme.getName(), host);
+                LOG.debug("{} Caching '{}' auth scheme for {}{}", exchangeId, authScheme.getName(), host,
+                        pathPrefix != null ? pathPrefix : "");
             }
-            authCache.put(host, authScheme);
+            authCache.put(host, pathPrefix, authScheme);
         }
     }
 
     private void clearCache(final HttpHost host,
+                            final String pathPrefix,
                             final HttpClientContext clientContext) {
         final AuthCache authCache = clientContext.getAuthCache();
         if (authCache != null) {
             if (LOG.isDebugEnabled()) {
                 final String exchangeId = clientContext.getExchangeId();
-                LOG.debug("{} Clearing cached auth scheme for {}", exchangeId, host);
+                LOG.debug("{} Clearing cached auth scheme for {}{}", exchangeId, host,
+                        pathPrefix != null ? pathPrefix : "");
             }
-            authCache.remove(host);
+            authCache.remove(host, pathPrefix);
         }
     }
 
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
index c22c57b..c0c92c7 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
@@ -32,6 +32,7 @@ import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -39,11 +40,12 @@ import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
-import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.net.NamedEndpoint;
 import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,7 +64,65 @@ public class BasicAuthCache implements AuthCache {
 
     private static final Logger LOG = LoggerFactory.getLogger(BasicAuthCache.class);
 
-    private final Map<HttpHost, byte[]> map;
+    static class Key {
+
+        final String scheme;
+        final String host;
+        final int port;
+        final String pathPrefix;
+
+        Key(final String scheme, final String host, final int port, final String pathPrefix) {
+            Args.notBlank(scheme, "Scheme");
+            Args.notBlank(host, "Scheme");
+            this.scheme = scheme.toLowerCase(Locale.ROOT);
+            this.host = host.toLowerCase(Locale.ROOT);
+            this.port = port;
+            this.pathPrefix = pathPrefix;
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj instanceof Key) {
+                final Key that = (Key) obj;
+                return this.scheme.equals(that.scheme) &&
+                        this.host.equals(that.host) &&
+                        this.port == that.port &&
+                        LangUtils.equals(this.pathPrefix, that.pathPrefix);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = LangUtils.HASH_SEED;
+            hash = LangUtils.hashCode(hash, this.scheme);
+            hash = LangUtils.hashCode(hash, this.host);
+            hash = LangUtils.hashCode(hash, this.port);
+            hash = LangUtils.hashCode(hash, this.pathPrefix);
+            return hash;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder buf = new StringBuilder();
+            buf.append(scheme).append("://").append(host);
+            if (port >= 0) {
+                buf.append(":").append(port);
+            }
+            if (pathPrefix != null) {
+                if (!pathPrefix.startsWith("/")) {
+                    buf.append("/");
+                }
+                buf.append(pathPrefix);
+            }
+            return buf.toString();
+        }
+    }
+
+    private final Map<Key, byte[]> map;
     private final SchemePortResolver schemePortResolver;
 
     /**
@@ -80,8 +140,27 @@ public class BasicAuthCache implements AuthCache {
         this(null);
     }
 
+    private Key key(final String scheme, final NamedEndpoint authority, final String pathPrefix) {
+        return new Key(scheme, authority.getHostName(), schemePortResolver.resolve(scheme, authority), pathPrefix);
+    }
+
     @Override
     public void put(final HttpHost host, final AuthScheme authScheme) {
+        put(host, null, authScheme);
+    }
+
+    @Override
+    public AuthScheme get(final HttpHost host) {
+        return get(host, null);
+    }
+
+    @Override
+    public void remove(final HttpHost host) {
+        remove(host, null);
+    }
+
+    @Override
+    public void put(final HttpHost host, final String pathPrefix, final AuthScheme authScheme) {
         Args.notNull(host, "HTTP host");
         if (authScheme == null) {
             return;
@@ -92,8 +171,7 @@ public class BasicAuthCache implements AuthCache {
                 try (final ObjectOutputStream out = new ObjectOutputStream(buf)) {
                     out.writeObject(authScheme);
                 }
-                final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
-                this.map.put(key, buf.toByteArray());
+                this.map.put(key(host.getSchemeName(), host, pathPrefix), buf.toByteArray());
             } catch (final IOException ex) {
                 if (LOG.isWarnEnabled()) {
                     LOG.warn("Unexpected I/O error while serializing auth scheme", ex);
@@ -107,10 +185,9 @@ public class BasicAuthCache implements AuthCache {
     }
 
     @Override
-    public AuthScheme get(final HttpHost host) {
+    public AuthScheme get(final HttpHost host, final String pathPrefix) {
         Args.notNull(host, "HTTP host");
-        final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
-        final byte[] bytes = this.map.get(key);
+        final byte[] bytes = this.map.get(key(host.getSchemeName(), host, pathPrefix));
         if (bytes != null) {
             try {
                 final ByteArrayInputStream buf = new ByteArrayInputStream(bytes);
@@ -131,10 +208,9 @@ public class BasicAuthCache implements AuthCache {
     }
 
     @Override
-    public void remove(final HttpHost host) {
+    public void remove(final HttpHost host, final String pathPrefix) {
         Args.notNull(host, "HTTP host");
-        final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
-        this.map.remove(key);
+        this.map.remove(key(host.getSchemeName(), host, pathPrefix));
     }
 
     @Override
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
index 5b80e21..45c0c5e 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
@@ -215,7 +215,7 @@ public final class ConnectExec implements ExecChainHandler {
         final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
 
         if (authCacheKeeper != null) {
-            authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context);
+            authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
         }
 
         ClassicHttpResponse response = null;
@@ -254,9 +254,9 @@ public final class ConnectExec implements ExecChainHandler {
 
                 if (authCacheKeeper != null) {
                     if (proxyAuthRequested) {
-                        authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                        authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                     } else {
-                        authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                        authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                     }
                 }
 
@@ -265,7 +265,7 @@ public final class ConnectExec implements ExecChainHandler {
                             proxyAuthStrategy, proxyAuthExchange, context);
 
                     if (authCacheKeeper != null) {
-                        authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                        authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                     }
                     if (updated) {
                         // Retry request
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
index 556f7a6..95f9545 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
@@ -42,6 +42,7 @@ import org.apache.hc.client5.http.classic.ExecChainHandler;
 import org.apache.hc.client5.http.classic.ExecRuntime;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.AuthSupport;
+import org.apache.hc.client5.http.impl.RequestSupport;
 import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
 import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -151,14 +152,26 @@ public final class ProtocolExec implements ExecChainHandler {
             }
 
             final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority());
+            final String pathPrefix = RequestSupport.extractPathPrefix(request);
 
             final AuthExchange targetAuthExchange = context.getAuthExchange(target);
             final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange();
 
+            if (!targetAuthExchange.isConnectionBased() &&
+                    targetAuthExchange.getPathPrefix() != null &&
+                    !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) {
+                // force re-authentication if the current path prefix does not match
+                // that of the previous authentication exchange.
+                targetAuthExchange.reset();
+            }
+            if (targetAuthExchange.getPathPrefix() == null) {
+                targetAuthExchange.setPathPrefix(pathPrefix);
+            }
+
             if (authCacheKeeper != null) {
-                authCacheKeeper.loadPreemptively(target, targetAuthExchange, context);
+                authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, context);
                 if (proxy != null) {
-                    authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -206,6 +219,7 @@ public final class ProtocolExec implements ExecChainHandler {
                         proxyAuthExchange,
                         proxy != null ? proxy : target,
                         target,
+                        pathPrefix,
                         response,
                         context)) {
                     // Make sure the response body is fully consumed, if present
@@ -259,6 +273,7 @@ public final class ProtocolExec implements ExecChainHandler {
             final AuthExchange proxyAuthExchange,
             final HttpHost proxy,
             final HttpHost target,
+            final String pathPrefix,
             final HttpResponse response,
             final HttpClientContext context) {
         final RequestConfig config = context.getRequestConfig();
@@ -268,9 +283,9 @@ public final class ProtocolExec implements ExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (targetAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context);
                 }
             }
 
@@ -279,9 +294,9 @@ public final class ProtocolExec implements ExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (proxyAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -290,7 +305,7 @@ public final class ProtocolExec implements ExecChainHandler {
                         targetAuthStrategy, targetAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
                 }
 
                 return updated;
@@ -300,7 +315,7 @@ public final class ProtocolExec implements ExecChainHandler {
                         proxyAuthStrategy, proxyAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                 }
 
                 return updated;
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java
deleted file mode 100644
index 0af8432..0000000
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl;
-
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.Method;
-import org.apache.hc.core5.http.message.BasicHttpRequest;
-import org.apache.hc.core5.net.URIAuthority;
-import org.hamcrest.CoreMatchers;
-import org.hamcrest.MatcherAssert;
-import org.junit.Test;
-
-/**
- * Simple tests for {@link ProtocolSupport}.
- */
-public class TestProtocolSupport {
-
-    @Test
-    public void testGetRequestUri() {
-        final HttpRequest request = new BasicHttpRequest(Method.GET, "");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("/"));
-        request.setAuthority(new URIAuthority("testUser", "localhost", 8080));
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("http://testUser@localhost:8080/"));
-        request.setScheme("https");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/"));
-        request.setPath("blah");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/blah"));
-        request.setPath("/blah/blah");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/blah/blah"));
-    }
-}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java
new file mode 100644
index 0000000..35e787b
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java
@@ -0,0 +1,53 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl;
+
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Simple tests for {@link RequestSupport}.
+ */
+public class TestRequestSupport {
+
+    @Test
+    public void testPathPrefixExtraction() {
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb")));
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/")));
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/../aaaa/")));
+        Assert.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/cccc")));
+        Assert.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/")));
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb?////")));
+        Assert.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aa%2faa/bbbb")));
+        Assert.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/a%61%2fa%61/bbbb")));
+        Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/")));
+        Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa")));
+        Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "")));
+    }
+
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
index b57fa6d..3486065 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
@@ -67,7 +67,7 @@ public class TestBasicAuthCache {
     }
 
     @Test
-    public void testStoreNonserializable() throws Exception {
+    public void testStoreNonSerializable() throws Exception {
         final BasicAuthCache cache = new BasicAuthCache();
         final AuthScheme authScheme = new NTLMScheme();
         cache.put(new HttpHost("localhost", 80), authScheme);
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java
deleted file mode 100644
index d6959c1..0000000
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.auth;
-
-import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.auth.AuthCache;
-import org.apache.hc.client5.http.auth.AuthExchange;
-import org.apache.hc.client5.http.auth.AuthScope;
-import org.apache.hc.client5.http.auth.Credentials;
-import org.apache.hc.client5.http.auth.CredentialsProvider;
-import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.http.protocol.RequestAuthCache;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.HttpRequestInterceptor;
-import org.apache.hc.core5.http.message.BasicHttpRequest;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-public class TestRequestAuthCache {
-
-    private HttpHost target;
-    private HttpHost proxy;
-    private Credentials creds1;
-    private Credentials creds2;
-    private AuthScope authscope1;
-    private AuthScope authscope2;
-    private BasicScheme authscheme1;
-    private BasicScheme authscheme2;
-    private CredentialsProvider credProvider;
-
-    @Before
-    public void setUp() {
-        this.target = new HttpHost("localhost", 80);
-        this.proxy = new HttpHost("localhost", 8080);
-
-        this.creds1 = new UsernamePasswordCredentials("user1", "secret1".toCharArray());
-        this.creds2 = new UsernamePasswordCredentials("user2", "secret2".toCharArray());
-        this.authscope1 = new AuthScope(this.target);
-        this.authscope2 = new AuthScope(this.proxy);
-        this.authscheme1 = new BasicScheme();
-        this.authscheme2 = new BasicScheme();
-
-        this.credProvider = CredentialsProviderBuilder.create()
-                .add(this.authscope1, this.creds1)
-                .add(this.authscope2, this.creds2)
-                .build();
-    }
-
-    @Test
-    public void testRequestParameterCheck() throws Exception {
-        final HttpClientContext context = HttpClientContext.create();
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        Assert.assertThrows(NullPointerException.class, () ->
-                interceptor.process(null, null, context));
-    }
-
-    @Test
-    public void testContextParameterCheck() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        Assert.assertThrows(NullPointerException.class, () ->
-                interceptor.process(request, null, null));
-    }
-
-    @Test
-    public void testPreemptiveTargetAndProxyAuth() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-
-        final AuthCache authCache = new BasicAuthCache();
-        authCache.put(this.target, this.authscheme1);
-        authCache.put(this.proxy, this.authscheme2);
-
-        context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNotNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNotNull(proxyAuthExchange.getAuthScheme());
-    }
-
-    @Test
-    public void testCredentialsProviderNotSet() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, null);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-
-        final AuthCache authCache = new BasicAuthCache();
-        authCache.put(this.target, this.authscheme1);
-        authCache.put(this.proxy, this.authscheme2);
-
-        context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNull(proxyAuthExchange.getAuthScheme());
-    }
-
-    @Test
-    public void testAuthCacheNotSet() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-        context.setAttribute(HttpClientContext.AUTH_CACHE, null);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNull(proxyAuthExchange.getAuthScheme());
-    }
-
-    @Test
-    public void testAuthCacheEmpty() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-
-        final AuthCache authCache = new BasicAuthCache();
-        context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNull(proxyAuthExchange.getAuthScheme());
-    }
-
-}