[java-support] branch master updated: OSJ-265: TLS socket factory clears client TLS credential too early

Brent Putman putmanb at georgetown.edu
Fri Mar 22 19:30:12 EDT 2019


This is an automated email from the git hooks/post-receive script.

putmanb pushed a commit to branch master
in repository java-support.

View the commit online:
http://git.shibboleth.net/view/?p=java-support.git;a=commit;h=cab2143c8460ac77ad4dc1ee4fdee8c26438a52b

The following commit(s) were added to refs/heads/master by this push:
       new  cab2143   OSJ-265: TLS socket factory clears client TLS credential too early
cab2143 is described below

commit cab2143c8460ac77ad4dc1ee4fdee8c26438a52b
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Wed Mar 13 20:04:46 2019 -0400

    OSJ-265: TLS socket factory clears client TLS credential too early
    
    This changes introduces a new feature of the HttpClient instances
    produced by HttpClientBuilder. The clients now automatically invoke
    instances of HttpClientContextHandler supplied either statically on
    the builder and/or dynamically via the HttpClientContext.
---
 .../httpclient/ContextHandlingHttpClient.java      |  379 ++++
 .../java/support/httpclient/HttpClientBuilder.java |   45 +-
 .../java/support/httpclient/HttpClientSupport.java |   53 +
 .../httpclient/ContextHandlingHttpClientTest.java  | 1962 ++++++++++++++++++++
 .../support/httpclient/HttpClientBuilderTest.java  |   55 +
 .../support/httpclient/HttpClientSupportTest.java  |   76 +
 6 files changed, 2559 insertions(+), 11 deletions(-)

diff --git a/src/main/java/net/shibboleth/utilities/java/support/httpclient/ContextHandlingHttpClient.java b/src/main/java/net/shibboleth/utilities/java/support/httpclient/ContextHandlingHttpClient.java
new file mode 100644
index 0000000..a90e544
--- /dev/null
+++ b/src/main/java/net/shibboleth/utilities/java/support/httpclient/ContextHandlingHttpClient.java
@@ -0,0 +1,379 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.
+ */
+
+package net.shibboleth.utilities.java.support.httpclient;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpRequestWrapper;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Lists;
+
+import net.shibboleth.utilities.java.support.collection.LazyList;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A wrapper implementation of {@link HttpClient} which invokes supplied instances of {@link HttpClientContextHandler}
+ * before and after request execution.
+ * 
+ * <p>
+ * By definition the handlers will only be invoked for the {@link HttpClient} execute(...) method variants
+ * which take an {@link HttpContext} argument.
+ * </p>
+ * 
+ * <p>
+ * The order of execution is:
+ * <ol>
+ * <li>Static handlers supplied via the constructor, in original list order</li>
+ * <li>Dynamic handlers from the context attribute {@link HttpClientSupport#CONTEXT_KEY_DYNAMIC_CONTEXT_HANDLERS},
+ *     in original list order</li>
+ * <li>the wrapped client's corresponding execute(...) method</li>
+ * <li>Dynamic handlers from the context attribute {@link HttpClientSupport#CONTEXT_KEY_DYNAMIC_CONTEXT_HANDLERS},
+ *     in reverse list order</li>
+ * <li>Static handlers supplied via the constructor, in reverse list order</li>
+ * </ol>
+ * </p>
+ */
+class ContextHandlingHttpClient implements HttpClient {
+    
+    /** Logger. */
+    private Logger log = LoggerFactory.getLogger(ContextHandlingHttpClient.class);
+    
+    /** The wrapped client instance. */
+    @Nonnull private HttpClient httpClient;
+    
+    /** Optional list of static handlers supplied to this class instance. */
+    @Nonnull private List<HttpClientContextHandler> handlers;
+    
+    /**
+     * Constructor.
+     *
+     * @param client the wrapped client instance
+     */
+    public ContextHandlingHttpClient(@Nonnull final HttpClient client) {
+        this(client, null);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param client the wrapped client instance
+     * @param staticHandlers the list of static handlers
+     */
+    public ContextHandlingHttpClient(@Nonnull final HttpClient client, 
+            @Nonnull final List<HttpClientContextHandler> staticHandlers) {
+        httpClient = Constraint.isNotNull(client, "HttpClient was null");
+        handlers = staticHandlers != null ? staticHandlers : Collections.emptyList();
+    }
+
+    /** {@inheritDoc} */
+    public HttpParams getParams() {
+        return httpClient.getParams();
+    }
+
+    /** {@inheritDoc} */
+    public ClientConnectionManager getConnectionManager() {
+        return httpClient.getConnectionManager();
+    }
+
+    /** {@inheritDoc} */
+    public HttpResponse execute(final HttpUriRequest request) throws IOException, ClientProtocolException {
+        return httpClient.execute(request);
+    }
+
+    /** {@inheritDoc} */
+    public HttpResponse execute(final HttpHost target, final HttpRequest request)
+            throws IOException, ClientProtocolException {
+        return httpClient.execute(target, request);
+    }
+
+    /** {@inheritDoc} */
+    public <T> T execute(final HttpUriRequest request, final ResponseHandler<? extends T> responseHandler)
+            throws IOException, ClientProtocolException {
+        return httpClient.execute(request, responseHandler);
+    }
+
+    /** {@inheritDoc} */
+    public <T> T execute(final HttpHost target, final HttpRequest request, 
+            final ResponseHandler<? extends T> responseHandler)
+            throws IOException, ClientProtocolException {
+        return httpClient.execute(target, request, responseHandler);
+    }
+
+    /** {@inheritDoc} */
+    public HttpResponse execute(final HttpUriRequest uriRequest, final HttpContext context)
+            throws IOException, ClientProtocolException {
+        
+        Throwable error = null;
+        
+        final HttpClientContext clientContext = HttpClientContext.adapt(context);
+        try {
+            invokeBefore(uriRequest, clientContext);
+            return httpClient.execute(uriRequest, context);
+        } catch (final Throwable t) {
+            error = t;
+            throw t;
+        } finally {
+            invokeAfter(uriRequest, clientContext, error);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public HttpResponse execute(final HttpHost target, final HttpRequest request, final HttpContext context)
+            throws IOException, ClientProtocolException {
+        
+        Throwable error = null;
+        
+        final HttpClientContext clientContext = HttpClientContext.adapt(context);
+        final HttpUriRequest uriRequest = HttpUriRequest.class.isInstance(request) 
+                ? (HttpUriRequest)request : HttpRequestWrapper.wrap(request, target);
+        try {
+            invokeBefore(uriRequest, clientContext);
+            return httpClient.execute(target, request, context);
+        } catch (final Throwable t) {
+            error = t;
+            throw t;
+        } finally {
+            invokeAfter(uriRequest, clientContext, error);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public <T> T execute(final HttpUriRequest uriRequest, final ResponseHandler<? extends T> responseHandler,
+            final HttpContext context) throws IOException, ClientProtocolException {
+        
+        Throwable error = null;
+        
+        final HttpClientContext clientContext = HttpClientContext.adapt(context);
+        try {
+            invokeBefore(uriRequest, clientContext);
+            return httpClient.execute(uriRequest, responseHandler, context);
+        } catch (final Throwable t) {
+            error = t;
+            throw t;
+        } finally {
+            invokeAfter(uriRequest, clientContext, error);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public <T> T execute(final HttpHost target, final HttpRequest request, 
+            final ResponseHandler<? extends T> responseHandler, final HttpContext context) 
+                    throws IOException, ClientProtocolException {
+        
+        Throwable error = null;
+        
+        final HttpClientContext clientContext = HttpClientContext.adapt(context);
+        final HttpUriRequest uriRequest = HttpUriRequest.class.isInstance(request) 
+                ? (HttpUriRequest)request : HttpRequestWrapper.wrap(request, target);
+        try {
+            invokeBefore(uriRequest, clientContext);
+            return httpClient.execute(target, request, responseHandler, context);
+        } catch (final Throwable t) {
+            error = t;
+            throw t;
+        } finally {
+            invokeAfter(uriRequest, clientContext, error);
+        }
+    }
+
+    /**
+     * Invoke {@link HttpClientContextHandler#invokeBefore(HttpClientContext, HttpUriRequest)}
+     * for supplied handlers.
+     * 
+     * @param request the HTTP request
+     * @param context the HTTP context
+     * @throws IOException if any handler throws an error
+     */
+    private void invokeBefore(final HttpUriRequest request, final HttpClientContext context) throws IOException {
+        log.trace("In invokeBefore");
+        
+        final List<Throwable> errors = new LazyList<>();
+
+        for (final HttpClientContextHandler handler : handlers) {
+            try {
+                if (handler != null) {
+                    log.trace("Invoking static handler invokeBefore: {}", handler.getClass().getName());
+                    handler.invokeBefore(context, request);
+                }
+            } catch (final Throwable t) {
+                log.warn("Static handler invokeBefore threw: {}", handler.getClass().getName(), t);
+                errors.add(t);
+            }
+        }
+
+        for (final HttpClientContextHandler handler 
+                : HttpClientSupport.getDynamicContextHandlerList(context)) {
+            try {
+                if (handler != null) {
+                    log.trace("Invoking dynamic handler invokeBefore: {}", handler.getClass().getName());
+                    handler.invokeBefore(context, request);
+                }
+            } catch (final Throwable t) {
+                log.warn("Dynamic handler invokeBefore threw: {}", handler.getClass().getName(), t);
+                errors.add(t);
+            }
+        }
+        
+        final IOException exception = processHandlerErrors("Invoke Before", errors);
+        if (exception != null) {
+            throw exception;
+        }
+        
+    }
+
+    /**
+     * Invoke {@link HttpClientContextHandler#invokeAfter(HttpClientContext, HttpUriRequest)}
+     * for all supplied handlers.
+     * 
+     * @param request the HTTP request
+     * @param context the HTTP context
+     * @param priorError an error thrown by by either {@link #invokeBefore(HttpUriRequest, HttpClientContext)}
+     *          or by HttpClient execute(...).
+     * 
+     * @throws IOException if any handler throws an error, or if priorError is an IOException. If priorError
+     *                     is a type of unchecked error (RuntimeException or Error) that will be propagated out
+     *                     here as well.
+     */
+    private void invokeAfter(final HttpUriRequest request, final HttpClientContext context, 
+            final Throwable priorError) throws IOException {
+        log.trace("In invokeAfter");
+        
+        final List<Throwable> errors = new LazyList<>();
+            
+        for (final HttpClientContextHandler handler 
+                : Lists.reverse(HttpClientSupport.getDynamicContextHandlerList(context))) {
+            try {
+                if (handler != null) {
+                    log.trace("Invoking dynamic handler invokeAfter: {}", handler.getClass().getName());
+                    handler.invokeAfter(context, request);
+                }
+            } catch (final Throwable t) {
+                log.warn("Dynamic handler invokeAfter threw: {}", handler.getClass().getName(), t);
+                errors.add(t);
+            }
+        }
+
+        for (final HttpClientContextHandler handler : Lists.reverse(handlers)) {
+            try {
+                if (handler != null) {
+                    log.trace("Invoking static handler invokeAfter: {}", handler.getClass().getName());
+                    handler.invokeAfter(context, request);
+                }
+            } catch (final Throwable t) {
+                log.warn("Static handler invokeAfter threw: {}", handler.getClass().getName(), t);
+                errors.add(t);
+            }
+        }
+        
+        final IOException exception = processHandlerErrors("Invoke After", errors);
+        processErrorsForInvokeAfter(exception, priorError);
+            
+    }
+    
+    /**
+     * Process the error(s) seen during {@link #invokeBefore(HttpUriRequest, HttpClientContext)} 
+     * or {@link #invokeAfter(HttpUriRequest, HttpClientContext, IOException, boolean)}
+     * into a single {@link IOException} that will be propagated out of that method.
+     * 
+     * @param stage the name of the stage, for reporting purposes
+     * @param errors all errors seen during the method execution
+     * 
+     * @return the single exception to be propagated out, will be null if no errors present
+     */
+    private IOException processHandlerErrors(final String stage, final List<Throwable> errors) {
+        if (errors == null || errors.isEmpty()) {
+            return null;
+        }
+        
+        if (errors.size() == 1) {
+            final Throwable t = errors.get(0);
+            if (IOException.class.isInstance(t)) {
+                return IOException.class.cast(t);
+            } else {
+                return new IOException(
+                        String.format("Context handler threw non-IOException Throwable in stage '%s'", stage), t);
+            }
+        } else {
+            final IOException e = new IOException(
+                    String.format("Multiple context handlers in stage '%s' reported error, see suppressed list", 
+                            stage));
+            for (final Throwable t : errors) {
+                e.addSuppressed(t);
+            }
+            return e;
+        }
+    }
+    
+    /**
+     * Process errors for
+     * {@link #invokeAfter(HttpUriRequest, HttpClientContext, Throwable)}.
+     * 
+     * @param invokeAfterException the exception thrown by invokeAfter handlers, if any
+     * @param priorError an error thrown by by either {@link #invokeBefore(HttpUriRequest, HttpClientContext)}
+     *          or by HttpClient execute(...), if any.
+     * 
+     * @throws IOException if invokeAfterException is non-null, or if priorError is an IOException. If priorError
+     *                     is a type of unchecked error (RuntimeException or Error) that will be propagated out
+     *                     here as well. 
+     */
+    private void processErrorsForInvokeAfter(final IOException invokeAfterException, final Throwable priorError)
+            throws IOException {
+        
+        if (priorError != null) {
+            if (invokeAfterException != null) {
+                priorError.addSuppressed(invokeAfterException);
+            }
+            // Note: The RuntimeException and Error cases below can only occur from the HttpClient execute() method,
+            // since a priorError from invokeBefore() is always processed into a checked IOException, and in that case
+            // HttpClient execute() wasn't called at all.
+            if (IOException.class.isInstance(priorError)) {
+                throw IOException.class.cast(priorError);
+            } else if (RuntimeException.class.isInstance(priorError)) {
+                throw RuntimeException.class.cast(priorError);
+            } else if (Error.class.isInstance(priorError)) {
+                throw Error.class.cast(priorError);
+            } else {
+                // This would either be an actual instance of java.lang.Throwable itself (not a subclass), 
+                // which really shouldn't ever happen,
+                // or some unaccounted case in a future version of Java which adds additional unchecked base types.
+                // For safety handle by converting to a RuntimeException.
+                throw new RuntimeException(priorError);
+            }
+        } else if (invokeAfterException != null) {
+            throw invokeAfterException;
+        }
+    }
+
+}
diff --git a/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilder.java b/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilder.java
index 3ad6284..6571d6c 100644
--- a/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilder.java
+++ b/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilder.java
@@ -250,6 +250,9 @@ public class HttpClientBuilder {
 
     /** List of response interceptors to add last. */
     @Nonnull @NonnullElements private List<HttpResponseInterceptor> responseInterceptorsLast;
+    
+    /** List of static context handlers. */
+    @Nonnull @NonnullElements private List<HttpClientContextHandler> staticContextHandlers;
 
     /** The Apache HttpClientBuilder 4.3+ instance over which to layer this builder. */
     private org.apache.http.impl.client.HttpClientBuilder apacheBuilder;
@@ -293,6 +296,7 @@ public class HttpClientBuilder {
         requestInterceptorsLast = Collections.emptyList();
         responseInterceptorsFirst = Collections.emptyList();
         responseInterceptorsLast = Collections.emptyList();
+        staticContextHandlers = Collections.emptyList();
     }
 
     /**
@@ -866,7 +870,7 @@ public class HttpClientBuilder {
      * @param interceptors the list of interceptors, may be null
      */
     public void setFirstRequestInterceptors(@Nullable final List<HttpRequestInterceptor> interceptors) {
-        requestInterceptorsFirst = (List<HttpRequestInterceptor>) normalizeInterceptors(interceptors);
+        requestInterceptorsFirst = (List<HttpRequestInterceptor>) normalizeList(interceptors);
     }
 
     /**
@@ -885,7 +889,7 @@ public class HttpClientBuilder {
      * @param interceptors the list of interceptors, may be null
      */
     public void setLastRequestInterceptors(@Nullable final List<HttpRequestInterceptor> interceptors) {
-        requestInterceptorsLast = normalizeInterceptors(interceptors);
+        requestInterceptorsLast = normalizeList(interceptors);
     }
 
     /**
@@ -904,7 +908,7 @@ public class HttpClientBuilder {
      * @param interceptors the list of interceptors, may be null
      */
     public void setFirstResponseInterceptors(@Nullable final List<HttpResponseInterceptor> interceptors) {
-        responseInterceptorsFirst = normalizeInterceptors(interceptors);
+        responseInterceptorsFirst = normalizeList(interceptors);
     }
 
     /**
@@ -923,22 +927,41 @@ public class HttpClientBuilder {
      * @param interceptors the list of interceptors, may be null
      */
     public void setLastResponseInterceptors(@Nullable final List<HttpResponseInterceptor> interceptors) {
-        responseInterceptorsLast = normalizeInterceptors(interceptors);
+        responseInterceptorsLast = normalizeList(interceptors);
+    }
+
+    /**
+     * Get the list of static {@link HttpClientContextHandler}.
+     * 
+     * @return the list of handlers
+     */
+    @Nonnull @NonnullElements @NotLive @Unmodifiable
+    public List<HttpClientContextHandler> getStaticContextHandlers() {
+        return ImmutableList.copyOf(staticContextHandlers);
+    }
+
+    /**
+     * Set the list of static {@link HttpClientContextHandler}.
+     * 
+     * @param handlers the list of handlers , may be null
+     */
+    public void setStaticContextHandlers(@Nullable final List<HttpClientContextHandler> handlers) {
+        staticContextHandlers = normalizeList(handlers);
     }
 
     /**
-     * Normalize and copy the supplied list of interceptors to remove nulls.
+     * Normalize and copy the supplied list to remove nulls.
      * 
      * @param <T> type of collection to normalize
      * 
-     * @param interceptors the list of interceptors to normalize
-     * @return copy of input list without nulls, may be null
+     * @param items the list of items to normalize
+     * @return copy of input list without nulls
      */
-    @Nonnull @NonnullElements private <T> List<T> normalizeInterceptors(@Nullable final List<T> interceptors) {
-        if (interceptors == null) {
+    @Nonnull @NonnullElements private <T> List<T> normalizeList(@Nullable final List<T> items) {
+        if (items == null) {
             return Collections.emptyList();
         } else {
-            return new ArrayList<>(Collections2.filter(interceptors, Predicates.notNull()));
+            return new ArrayList<>(Collections2.filter(items, Predicates.notNull()));
         }
     }
 
@@ -951,7 +974,7 @@ public class HttpClientBuilder {
      */
     public HttpClient buildClient() throws Exception {
         decorateApacheBuilder();
-        return getApacheBuilder().build();
+        return new ContextHandlingHttpClient(getApacheBuilder().build(), getStaticContextHandlers());
     }
 
     /**
diff --git a/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientSupport.java b/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientSupport.java
index 552135e..2d4c63b 100644
--- a/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientSupport.java
+++ b/src/main/java/net/shibboleth/utilities/java/support/httpclient/HttpClientSupport.java
@@ -28,7 +28,9 @@ import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -38,6 +40,7 @@ import javax.net.ssl.X509TrustManager;
 
 import org.apache.http.HttpEntity;
 import org.apache.http.ParseException;
+import org.apache.http.client.protocol.HttpClientContext;
 import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
 import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
@@ -49,6 +52,7 @@ import org.apache.http.util.CharArrayBuffer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
 
@@ -57,6 +61,11 @@ import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.Object
  */
 public final class HttpClientSupport {
     
+    /** Context key for instances of dynamic context handlers to be invoked before and after the HTTP request.
+     * Must be an instance of
+     * {@link java.util.List}<code><</code>{@link HttpClientContextHandler}<code>></code>. */
+    private static final String CONTEXT_KEY_DYNAMIC_CONTEXT_HANDLERS = "java-support.DynamicContextHandlers";
+
     /** Constructor to prevent instantiation. */
     private HttpClientSupport() { }
     
@@ -175,6 +184,50 @@ public final class HttpClientSupport {
         
     }
 
+    /**
+     * Get the list of {@link HttpClientContextHandler} for the {@link HttpClientContext}.
+     *
+     * @param context the client context
+     * @return the handler list
+     */
+    @Nonnull public static List<HttpClientContextHandler> getDynamicContextHandlerList(
+            @Nonnull final HttpClientContext context) {
+        Constraint.isNotNull(context, "HttpClientContext was null");
+        List<HttpClientContextHandler> handlers =
+                context.getAttribute(CONTEXT_KEY_DYNAMIC_CONTEXT_HANDLERS, List.class);
+        if (handlers == null) {
+            handlers = new ArrayList<>();
+            context.setAttribute(CONTEXT_KEY_DYNAMIC_CONTEXT_HANDLERS, handlers);
+        }
+        return handlers;
+    }
+
+    /**
+     * Add the specified instance of {@link HttpClientContextHandler}
+     * to the {@link HttpClientContext} in the first handler list position.
+     *
+     * @param context the client context
+     * @param handler the handler to add
+     */
+    public static void addDynamicContextHandlerFirst(@Nonnull final HttpClientContext context,
+            @Nonnull final HttpClientContextHandler handler) {
+        Constraint.isNotNull(handler, "HttpClientContextHandler was null");
+        getDynamicContextHandlerList(context).add(0, handler);
+    }
+
+    /**
+     * Add the specified instance of {@link HttpClientContextHandler}
+     * to the {@link HttpClientContext} in the last handler list position.
+     *
+     * @param context the client context
+     * @param handler the handler to add
+     */
+    public static void addDynamicContextHandlerLast(@Nonnull final HttpClientContext context,
+            @Nonnull final HttpClientContextHandler handler) {
+        Constraint.isNotNull(handler, "HttpClientContextHandler was null");
+        getDynamicContextHandlerList(context).add(handler);
+    }
+
 // Checkstyle: CyclomaticComplexity OFF
     /**
      * Get the entity content as a String, using the provided default character set
diff --git a/src/test/java/net/shibboleth/utilities/java/support/httpclient/ContextHandlingHttpClientTest.java b/src/test/java/net/shibboleth/utilities/java/support/httpclient/ContextHandlingHttpClientTest.java
new file mode 100644
index 0000000..1936ce8
--- /dev/null
+++ b/src/test/java/net/shibboleth/utilities/java/support/httpclient/ContextHandlingHttpClientTest.java
@@ -0,0 +1,1962 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.
+ */
+
+package net.shibboleth.utilities.java.support.httpclient;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HttpContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.Lists;
+
+public class ContextHandlingHttpClientTest {
+    
+    private ContextHandlingHttpClient client;
+    
+    private TestContextHandler staticOne, staticTwo, staticThree;
+    private TestContextHandler dynamicOne, dynamicTwo, dynamicThree;
+    
+    private HttpClientContext context;
+    
+    private HttpUriRequest request;
+    private HttpHost target;
+    private ResponseHandler<Object> responseHandler = new MockResponseHandler();
+    
+    @BeforeClass
+    public void setupClass() {
+        staticOne = new TestContextHandler("static-1");
+        staticTwo = new TestContextHandler("static-2");
+        staticThree = new TestContextHandler("static-3");
+        dynamicOne = new TestContextHandler("dynamic-1");
+        dynamicTwo = new TestContextHandler("dynamic-2");
+        dynamicThree = new TestContextHandler("dynamic-3");
+        
+    }
+    
+    @BeforeMethod
+    public void setupMethod() {
+        request = new HttpGet("/test");
+        target = new HttpHost("test.example.edu");
+    }
+    
+    @Test
+    public void testNoHandlers() throws ClientProtocolException, IOException {
+        client = new ContextHandlingHttpClient(new MockHttpClient());
+        context = HttpClientContext.create();
+        
+        //Non-context execute methods
+        Assert.assertSame(client.execute(request), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertSame(client.execute(request, responseHandler), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertSame(client.execute(target, request), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertSame(client.execute(target, request, responseHandler), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        
+        //Context execute methods
+        Assert.assertSame(client.execute(request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertSame(client.execute(request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertSame(client.execute(target, request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertSame(client.execute(target, request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+    }
+    
+    @Test
+    public void testStaticOnly() throws ClientProtocolException, IOException {
+        client = new ContextHandlingHttpClient(new MockHttpClient(), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        context = HttpClientContext.create();
+        Assert.assertSame(client.execute(request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        Assert.assertSame(client.execute(request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        Assert.assertSame(client.execute(target, request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        Assert.assertSame(client.execute(target, request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+    }
+    
+    @Test
+    public void testDynamicOnly() throws ClientProtocolException, IOException {
+        client = new ContextHandlingHttpClient(new MockHttpClient());
+        
+        List<String> control = Lists.newArrayList(
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(target, request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(target, request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+    }
+    
+    @Test
+    public void testStaticAndDynamic() throws ClientProtocolException, IOException {
+        client = new ContextHandlingHttpClient(new MockHttpClient(), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(target, request, context), MockHttpClient.STATIC_RESPONSE_HTTP);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        
+        context = HttpClientContext.create();
+        HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+        Assert.assertSame(client.execute(target, request, responseHandler, context), MockHttpClient.STATIC_RESPONSE_HANDLER);
+        Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+    }
+    
+    @Test
+    public void testWrappedClientThrowsIOException() throws ClientProtocolException, IOException {
+        IOException error = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(error), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testWrappedClientThrowsRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(error), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (RuntimeException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (RuntimeException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (RuntimeException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (RuntimeException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testWrappedClientThrowsError() throws ClientProtocolException, IOException {
+        Error error = new Error();
+        client = new ContextHandlingHttpClient(new MockHttpClient(error), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (Error e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (Error e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (Error e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (Error e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+     
+    @Test
+    public void testSingleStaticHandlerInvokeBeforeThrowsIOException() throws ClientProtocolException, IOException {
+        IOException error = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", error, null), staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testSingleStaticHandlerInvokeAfterThrowsIOException() throws ClientProtocolException, IOException {
+        IOException error = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", null, error), staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testSingleStaticHandlerInvokeBeforeThrowsRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", error, null), staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testSingleStaticHandlerInvokeAfterThrowsRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", null, error), staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testMultipleStaticHandlersInvokeBeforeThrowIOException() throws ClientProtocolException, IOException {
+        IOException error1 = new IOException();
+        IOException error3 = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList( new TestContextHandler("static-1", error1, null), staticTwo, new TestContextHandler("static-3", error3, null)));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testMultipleStaticHandlersInvokeBeforeThrowRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error1 = new RuntimeException();
+        RuntimeException error3 = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList( new TestContextHandler("static-1", error1, null), staticTwo, new TestContextHandler("static-3", error3, null)));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    
+    @Test
+    public void testMultipleStaticHandlersInvokeAfterThrowIOException() throws ClientProtocolException, IOException {
+        IOException error1 = new IOException();
+        IOException error3 = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList( new TestContextHandler("static-1", null, error1), staticTwo, new TestContextHandler("static-3", null, error3)));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testMultipleStaticHandlersInvokeAfterThrowRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error1 = new RuntimeException();
+        RuntimeException error3 = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList( new TestContextHandler("static-1", null, error1), staticTwo, new TestContextHandler("static-3", null, error3)));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, dynamicTwo, dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    
+    @Test
+    public void testSingleDynamicHandlerInvokeBeforeThrowsIOException() throws ClientProtocolException, IOException {
+        IOException error = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", error, null), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testSingleDynamicHandlerInvokeAfterThrowsIOException() throws ClientProtocolException, IOException {
+        IOException error = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, error), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    
+    @Test
+    public void testSingleDynamicHandlerInvokeBeforeThrowsRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", error, null), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testSingleDynamicHandlerInvokeAfterThrowsRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, error), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), error);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    
+    @Test
+    public void testMultipleDynamicHandlersInvokeBeforeThrowIOException() throws ClientProtocolException, IOException {
+        IOException error1 = new IOException();
+        IOException error3 = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = 
+                Lists.newArrayList(new TestContextHandler("dynamic-1", error1, null), dynamicTwo, new TestContextHandler("dynamic-3", error3, null));
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testMultipleDynamicHandlersInvokeBeforeThrowRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error1 = new RuntimeException();
+        RuntimeException error3 = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = 
+                Lists.newArrayList(new TestContextHandler("dynamic-1", error1, null), dynamicTwo, new TestContextHandler("dynamic-3", error3, null));
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testMultipleDynamicHandlersInvokeAfterThrowIOException() throws ClientProtocolException, IOException {
+        IOException error1 = new IOException();
+        IOException error3 = new IOException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = 
+                Lists.newArrayList(new TestContextHandler("dynamic-1", null, error1), dynamicTwo, new TestContextHandler("dynamic-3", null, error3));
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testMultipleDynamicHandlersInvokeAfterThrowRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException error1 = new RuntimeException();
+        RuntimeException error3 = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), Lists.newArrayList(staticOne, staticTwo, staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = 
+                Lists.newArrayList(new TestContextHandler("dynamic-1", null, error1), dynamicTwo, new TestContextHandler("dynamic-3", null, error3));
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertNotSame(e, error1);
+            Assert.assertNotSame(e, error3);
+            Assert.assertEquals(e.getSuppressed().length, 2);
+            Assert.assertTrue(Arrays.asList(e.getSuppressed()).containsAll(Lists.newArrayList(error1, error3)));
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    
+    @Test
+    public void testStaticAndDynamicHandlersThrowIOException() throws ClientProtocolException, IOException {
+        IOException staticBeforeError = null;
+        IOException dynamicAfterError = null;
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> dynamicHandlers = null;
+        
+        staticBeforeError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", staticBeforeError, null), staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        staticBeforeError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", staticBeforeError, null), staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        staticBeforeError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", staticBeforeError, null), staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        staticBeforeError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", staticBeforeError, null), staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testStaticAndDynamicHandlersThrowRuntimeException() throws ClientProtocolException, IOException {
+        RuntimeException staticBeforeError = new RuntimeException();
+        RuntimeException dynamicAfterError = new RuntimeException();
+        client = new ContextHandlingHttpClient(new MockHttpClient(), 
+                Lists.newArrayList(staticOne, new TestContextHandler("static-2", staticBeforeError, null), staticThree));
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> handlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0].getCause(), dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0].getCause(), dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0].getCause(), dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(handlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e.getCause(), staticBeforeError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0].getCause(), dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+    
+    @Test
+    public void testWrappedClientThrowsIOExceptionInvokeAfterThrowsIOException() throws ClientProtocolException, IOException {
+        IOException clientError = null;
+        IOException dynamicAfterError = null;
+        
+        List<String> control = Lists.newArrayList(
+                "before-static-1",
+                "before-static-2",
+                "before-static-3",
+                "before-dynamic-1",
+                "before-dynamic-2",
+                "before-dynamic-3",
+                "after-dynamic-3",
+                "after-dynamic-2",
+                "after-dynamic-1",
+                "after-static-3",
+                "after-static-2",
+                "after-static-1"
+                );
+        
+        List<HttpClientContextHandler> dynamicHandlers = null;
+        
+        clientError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(clientError), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, clientError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        clientError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(clientError), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, clientError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        clientError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(clientError), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(target, request, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, clientError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+        
+        clientError = new IOException();
+        dynamicAfterError = new IOException();
+        
+        client = new ContextHandlingHttpClient(new MockHttpClient(clientError), 
+                Lists.newArrayList(staticOne, staticTwo, staticThree));
+        dynamicHandlers = Lists.newArrayList(dynamicOne, new TestContextHandler("dynamic-2", null, dynamicAfterError), dynamicThree);
+        
+        try {
+            context = HttpClientContext.create();
+            HttpClientSupport.getDynamicContextHandlerList(context).addAll(dynamicHandlers);
+            client.execute(target, request, responseHandler, context);
+            Assert.fail("Wrapped client should have thrown");
+        } catch (IOException e) {
+            Assert.assertSame(e, clientError);
+            Assert.assertEquals(e.getSuppressed().length, 1);
+            Assert.assertSame(e.getSuppressed()[0], dynamicAfterError);
+            Assert.assertEquals(context.getAttribute(TestContextHandler.TEST_KEY), control);
+        }
+    }
+     
+    
+    
+    
+    
+    // Helpers
+    
+    private class TestContextHandler implements HttpClientContextHandler {
+        
+        public static final String TEST_KEY = "testKey";
+        
+        private String name;
+        
+        private Throwable invokeBeforeError;
+        private Throwable invokeAfterError;
+        
+        public TestContextHandler(String instanceName) {
+            name = instanceName;
+        }
+        
+        public TestContextHandler(Throwable beforeError, Throwable afterError) {
+            invokeBeforeError = beforeError;
+            invokeAfterError = afterError;
+        }
+        
+        public TestContextHandler(String instanceName, Throwable beforeError, Throwable afterError) {
+            name = instanceName;
+            invokeBeforeError = beforeError;
+            invokeAfterError = afterError;
+        }
+        
+        /** {@inheritDoc} */
+        public void invokeBefore(HttpClientContext context, HttpUriRequest request) throws IOException {
+            if (name != null) {
+                addValue(context, "before-" + name);
+            }
+            ThrowableHelper.checkAndThrowError(invokeBeforeError);
+        }
+
+        /** {@inheritDoc} */
+        public void invokeAfter(HttpClientContext context, HttpUriRequest request) throws IOException {
+            if (name != null) {
+                addValue(context, "after-" + name);
+            }
+            ThrowableHelper.checkAndThrowError(invokeAfterError);
+        }
+        
+        private void addValue(HttpClientContext context, String value) {
+            List<String> attrib = context.getAttribute(TEST_KEY, List.class);
+            if (attrib == null) {
+                attrib = new ArrayList<>();
+                context.setAttribute(TEST_KEY, attrib);
+            }
+            attrib.add(value);
+        }
+        
+    }
+    
+    private static class MockHttpClient implements HttpClient {
+        
+        public static final HttpResponse STATIC_RESPONSE_HTTP = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), HttpStatus.SC_OK, "OK");
+        
+        public static final Object STATIC_RESPONSE_HANDLER = new Object();
+        
+        private Throwable error;
+        
+        public MockHttpClient() {
+        }
+        
+        public MockHttpClient(final Throwable throwable) {
+            error = throwable;
+        }
+
+        /** {@inheritDoc} */
+        public HttpParams getParams() {
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        public ClientConnectionManager getConnectionManager() {
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        public HttpResponse execute(HttpUriRequest request) throws IOException, ClientProtocolException {
+            return responseHTTP();
+        }
+
+        /** {@inheritDoc} */
+        public HttpResponse execute(HttpUriRequest request, HttpContext context)
+                throws IOException, ClientProtocolException {
+            return responseHTTP();
+        }
+
+        /** {@inheritDoc} */
+        public HttpResponse execute(HttpHost target, HttpRequest request) throws IOException, ClientProtocolException {
+            return responseHTTP();
+        }
+
+        /** {@inheritDoc} */
+        public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)
+                throws IOException, ClientProtocolException {
+            return responseHTTP();
+        }
+
+        /** {@inheritDoc} */
+        public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler)
+                throws IOException, ClientProtocolException {
+           return responseHandler();
+        }
+
+        /** {@inheritDoc} */
+        public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
+                throws IOException, ClientProtocolException {
+            return responseHandler();
+        }
+
+        /** {@inheritDoc} */
+        public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)
+                throws IOException, ClientProtocolException {
+            return responseHandler();
+        }
+
+        /** {@inheritDoc} */
+        public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler,
+                HttpContext context) throws IOException, ClientProtocolException {
+            return responseHandler();
+        }
+        
+        private HttpResponse responseHTTP() throws IOException {
+            ThrowableHelper.checkAndThrowError(error);
+            return STATIC_RESPONSE_HTTP;
+        }
+        
+        private <T> T responseHandler() throws IOException {
+            ThrowableHelper.checkAndThrowError(error);
+            return (T) STATIC_RESPONSE_HANDLER;
+        }
+        
+    }
+
+    public static class MockResponseHandler implements ResponseHandler<Object> {
+
+        public Object handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
+            return null;
+        }
+
+    }
+    
+    public static class ThrowableHelper {
+        
+        public static void checkAndThrowError(Throwable t) throws IOException {
+            if (t != null) {
+                if (IOException.class.isInstance(t)) {
+                    throw IOException.class.cast(t);
+                } 
+                if (RuntimeException.class.isInstance(t)) {
+                    throw RuntimeException.class.cast(t);
+                } 
+                if (Error.class.isInstance(t)) {
+                    throw Error.class.cast(t);
+                } 
+            }
+        }
+        
+    }
+
+}
diff --git a/src/test/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilderTest.java b/src/test/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilderTest.java
index 3cf0409..d8d0369 100644
--- a/src/test/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilderTest.java
+++ b/src/test/java/net/shibboleth/utilities/java/support/httpclient/HttpClientBuilderTest.java
@@ -18,12 +18,17 @@
 
 package net.shibboleth.utilities.java.support.httpclient;
 
+import java.io.IOException;
 import java.time.Duration;
 
 import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.Lists;
+
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
 public class HttpClientBuilderTest {
@@ -49,4 +54,54 @@ public class HttpClientBuilderTest {
         Assert.assertNotNull(client);
     }
     
+    @Test
+    public void testContextHandlingSupport() throws Exception {
+        
+        HttpClientContextHandler handler1 = new TestContextHandler();
+        HttpClientContextHandler handler2 = new TestContextHandler();
+        HttpClientContextHandler handler3 = new TestContextHandler();
+        
+        final HttpClientBuilder builder = new HttpClientBuilder();
+        
+        Assert.assertNotNull(builder.getStaticContextHandlers());
+        Assert.assertTrue(builder.getStaticContextHandlers().isEmpty());
+        
+        builder.setStaticContextHandlers(Lists.newArrayList(null, handler1, null, handler2, null, handler3));
+        Assert.assertEquals(builder.getStaticContextHandlers(), Lists.newArrayList(handler1, handler2, handler3));
+        
+        try {
+            builder.getStaticContextHandlers().add(new TestContextHandler());
+            Assert.fail("List should have been unmodifaible");
+        } catch (UnsupportedOperationException e) {
+            //expected
+        }
+        
+        builder.resetDefaults();
+        
+        Assert.assertNotNull(builder.getStaticContextHandlers());
+        Assert.assertTrue(builder.getStaticContextHandlers().isEmpty());
+        
+        final HttpClient client = builder.buildClient();
+        Assert.assertNotNull(client);
+        Assert.assertTrue(ContextHandlingHttpClient.class.isInstance(client));
+        
+    }
+    
+    //Helpers 
+    
+    public class TestContextHandler implements HttpClientContextHandler {
+
+        /** {@inheritDoc} */
+        public void invokeBefore(HttpClientContext context, HttpUriRequest request) throws IOException {
+            
+        }
+
+        /** {@inheritDoc} */
+        public void invokeAfter(HttpClientContext context, HttpUriRequest request) throws IOException {
+            
+        }
+        
+    }
+     
+    
 }
diff --git a/src/test/java/net/shibboleth/utilities/java/support/httpclient/HttpClientSupportTest.java b/src/test/java/net/shibboleth/utilities/java/support/httpclient/HttpClientSupportTest.java
new file mode 100644
index 0000000..4eb37ee
--- /dev/null
+++ b/src/test/java/net/shibboleth/utilities/java/support/httpclient/HttpClientSupportTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.
+ */
+
+package net.shibboleth.utilities.java.support.httpclient;
+
+import java.io.IOException;
+
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.Lists;
+
+public class HttpClientSupportTest {
+
+    @Test
+    public void testAddDynamicContextHandlerFirst() {
+        HttpClientContext context = HttpClientContext.create();
+        HttpClientContextHandler one = new TestContextHandler();
+        HttpClientContextHandler two = new TestContextHandler();
+        HttpClientContextHandler three = new TestContextHandler();
+        
+        HttpClientSupport.addDynamicContextHandlerFirst(context, one);
+        HttpClientSupport.addDynamicContextHandlerFirst(context, two);
+        HttpClientSupport.addDynamicContextHandlerFirst(context, three);
+        
+        Assert.assertEquals(HttpClientSupport.getDynamicContextHandlerList(context), Lists.newArrayList(three, two, one));
+    }
+     
+    @Test
+    public void testAddDynamicContextHandlerLast() {
+        HttpClientContext context = HttpClientContext.create();
+        HttpClientContextHandler one = new TestContextHandler();
+        HttpClientContextHandler two = new TestContextHandler();
+        HttpClientContextHandler three = new TestContextHandler();
+        
+        HttpClientSupport.addDynamicContextHandlerLast(context, one);
+        HttpClientSupport.addDynamicContextHandlerLast(context, two);
+        HttpClientSupport.addDynamicContextHandlerLast(context, three);
+        
+        Assert.assertEquals(HttpClientSupport.getDynamicContextHandlerList(context), Lists.newArrayList(one, two, three));
+    }
+     
+    
+    //Helpers 
+    
+    public class TestContextHandler implements HttpClientContextHandler {
+
+        /** {@inheritDoc} */
+        public void invokeBefore(HttpClientContext context, HttpUriRequest request) throws IOException {
+            
+        }
+
+        /** {@inheritDoc} */
+        public void invokeAfter(HttpClientContext context, HttpUriRequest request) throws IOException {
+            
+        }
+        
+    }
+    
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list