聊聊HttpClient的RedirectStrategy

2023-10-18 09:16:56 浏览数 (2)

本文主要研究一下HttpClient的RedirectStrategy

RedirectStrategy

org/apache/http/client/RedirectStrategy.java

代码语言:javascript复制
public interface RedirectStrategy {

    /**
     * Determines if a request should be redirected to a new location
     * given the response from the target server.
     *
     * @param request the executed request
     * @param response the response received from the target server
     * @param context the context for the request execution
     *
     * @return {@code true} if the request should be redirected, {@code false}
     * otherwise
     */
    boolean isRedirected(
            HttpRequest request,
            HttpResponse response,
            HttpContext context) throws ProtocolException;

    /**
     * Determines the redirect location given the response from the target
     * server and the current request execution context and generates a new
     * request to be sent to the location.
     *
     * @param request the executed request
     * @param response the response received from the target server
     * @param context the context for the request execution
     *
     * @return redirected request
     */
    HttpUriRequest getRedirect(
            HttpRequest request,
            HttpResponse response,
            HttpContext context) throws ProtocolException;

}

RedirectStrategy接口定义了isRedirected方法用于判断是否需要redirect,还定义了getRedirect方法用于返回redirect的目标地址

DefaultRedirectStrategy

org/apache/http/impl/client/DefaultRedirectStrategy.java

代码语言:javascript复制
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class DefaultRedirectStrategy implements RedirectStrategy {

    private final Log log = LogFactory.getLog(getClass());

    /**
     * @deprecated (4.3) use {@link org.apache.http.client.protocol.HttpClientContext#REDIRECT_LOCATIONS}.
     */
    @Deprecated
    public static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";

    public static final DefaultRedirectStrategy INSTANCE = new DefaultRedirectStrategy();

    private final String[] redirectMethods;

    public DefaultRedirectStrategy() {
        this(new String[] {
            HttpGet.METHOD_NAME,
            HttpHead.METHOD_NAME
        });
    }

    /**
     * Constructs a new instance to redirect the given HTTP methods.
     *
     * @param redirectMethods The methods to redirect.
     * @since 4.5.10
     */
    public DefaultRedirectStrategy(final String[] redirectMethods) {
        super();
        final String[] tmp = redirectMethods.clone();
        Arrays.sort(tmp);
        this.redirectMethods = tmp;
    }

    @Override
    public boolean isRedirected(
            final HttpRequest request,
            final HttpResponse response,
            final HttpContext context) throws ProtocolException {
        Args.notNull(request, "HTTP request");
        Args.notNull(response, "HTTP response");

        final int statusCode = response.getStatusLine().getStatusCode();
        final String method = request.getRequestLine().getMethod();
        final Header locationHeader = response.getFirstHeader("location");
        switch (statusCode) {
        case HttpStatus.SC_MOVED_TEMPORARILY:
            return isRedirectable(method) && locationHeader != null;
        case HttpStatus.SC_MOVED_PERMANENTLY:
        case HttpStatus.SC_TEMPORARY_REDIRECT:
            return isRedirectable(method);
        case HttpStatus.SC_SEE_OTHER:
            return true;
        default:
            return false;
        } //end of switch
    }

    /**
     * @since 4.2
     */
    protected boolean isRedirectable(final String method) {
        return Arrays.binarySearch(redirectMethods, method) >= 0;
    }

    @Override
    public HttpUriRequest getRedirect(
            final HttpRequest request,
            final HttpResponse response,
            final HttpContext context) throws ProtocolException {
        final URI uri = getLocationURI(request, response, context);
        final String method = request.getRequestLine().getMethod();
        if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) {
            return new HttpHead(uri);
        } else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) {
            return new HttpGet(uri);
        } else {
            final int status = response.getStatusLine().getStatusCode();
            return status == HttpStatus.SC_TEMPORARY_REDIRECT
                            ? RequestBuilder.copy(request).setUri(uri).build()
                            : new HttpGet(uri);
        }
    }

}    

DefaultRedirectStrategy实现了RedirectStrategy接口,它定义了redirectMethods,默认是Get和Head;isRedirected方法先获取response的statusCode,对于302需要location的header有值且请求method在redirectMethods中(isRedirectable),对于301及307仅仅判断isRedirectable,对于303返回true,其余的返回false

getRedirect方法先通过getLocationURI获取目标地址,然后针对get或者head分别构造HttpHead及HttpGet,剩下的根据statusCode判断,是307则拷贝原来的request,否则返回HttpGet

RedirectExec

org/apache/http/impl/execchain/RedirectExec.java

代码语言:javascript复制
/**
 * Request executor in the request execution chain that is responsible
 * for handling of request redirects.
 * <p>
 * Further responsibilities such as communication with the opposite
 * endpoint is delegated to the next executor in the request execution
 * chain.
 * </p>
 *
 * @since 4.3
 */
@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
public class RedirectExec implements ClientExecChain {

    private final Log log = LogFactory.getLog(getClass());

    private final ClientExecChain requestExecutor;
    private final RedirectStrategy redirectStrategy;
    private final HttpRoutePlanner routePlanner;

    public RedirectExec(
            final ClientExecChain requestExecutor,
            final HttpRoutePlanner routePlanner,
            final RedirectStrategy redirectStrategy) {
        super();
        Args.notNull(requestExecutor, "HTTP client request executor");
        Args.notNull(routePlanner, "HTTP route planner");
        Args.notNull(redirectStrategy, "HTTP redirect strategy");
        this.requestExecutor = requestExecutor;
        this.routePlanner = routePlanner;
        this.redirectStrategy = redirectStrategy;
    }

    @Override
    public CloseableHttpResponse execute(
            final HttpRoute route,
            final HttpRequestWrapper request,
            final HttpClientContext context,
            final HttpExecutionAware execAware) throws IOException, HttpException {
        Args.notNull(route, "HTTP route");
        Args.notNull(request, "HTTP request");
        Args.notNull(context, "HTTP context");

        final List<URI> redirectLocations = context.getRedirectLocations();
        if (redirectLocations != null) {
            redirectLocations.clear();
        }

        final RequestConfig config = context.getRequestConfig();
        final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
        HttpRoute currentRoute = route;
        HttpRequestWrapper currentRequest = request;
        for (int redirectCount = 0;;) {
            final CloseableHttpResponse response = requestExecutor.execute(
                    currentRoute, currentRequest, context, execAware);
            try {
                if (config.isRedirectsEnabled() &&
                        this.redirectStrategy.isRedirected(currentRequest.getOriginal(), response, context)) {

                    if (redirectCount >= maxRedirects) {
                        throw new RedirectException("Maximum redirects ("  maxRedirects   ") exceeded");
                    }
                    redirectCount  ;

                    final HttpRequest redirect = this.redirectStrategy.getRedirect(
                            currentRequest.getOriginal(), response, context);
                    if (!redirect.headerIterator().hasNext()) {
                        final HttpRequest original = request.getOriginal();
                        redirect.setHeaders(original.getAllHeaders());
                    }
                    currentRequest = HttpRequestWrapper.wrap(redirect);

                    if (currentRequest instanceof HttpEntityEnclosingRequest) {
                        RequestEntityProxy.enhance((HttpEntityEnclosingRequest) currentRequest);
                    }

                    final URI uri = currentRequest.getURI();
                    final HttpHost newTarget = URIUtils.extractHost(uri);
                    if (newTarget == null) {
                        throw new ProtocolException("Redirect URI does not specify a valid host name: "  
                                uri);
                    }

                    // Reset virtual host and auth states if redirecting to another host
                    if (!currentRoute.getTargetHost().equals(newTarget)) {
                        final AuthState targetAuthState = context.getTargetAuthState();
                        if (targetAuthState != null) {
                            this.log.debug("Resetting target auth state");
                            targetAuthState.reset();
                        }
                        final AuthState proxyAuthState = context.getProxyAuthState();
                        if (proxyAuthState != null && proxyAuthState.isConnectionBased()) {
                            this.log.debug("Resetting proxy auth state");
                            proxyAuthState.reset();
                        }
                    }

                    currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context);
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Redirecting to '"   uri   "' via "   currentRoute);
                    }
                    EntityUtils.consume(response.getEntity());
                    response.close();
                } else {
                    return response;
                }
            } catch (final RuntimeException ex) {
                response.close();
                throw ex;
            } catch (final IOException ex) {
                response.close();
                throw ex;
            } catch (final HttpException ex) {
                // Protocol exception related to a direct.
                // The underlying connection may still be salvaged.
                try {
                    EntityUtils.consume(response.getEntity());
                } catch (final IOException ioex) {
                    this.log.debug("I/O error while releasing connection", ioex);
                } finally {
                    response.close();
                }
                throw ex;
            }
        }
    }

}

RedirectExec实现了ClientExecChain接口,其构造器要求传入requestExecutor、redirectStrategy、routePlanner,其execute方法会先获取maxRedirects参数,然后执行requestExecutor.execute,接着在config.isRedirectsEnabled()以及redirectStrategy.isRedirected为true时才进入redirect逻辑,它会先判断是否超出maxRedirects,大于等于则抛出RedirectException,否则通过redirectStrategy.getRedirect获取HttpRequest,更新currentRoute,然后清理entity关闭response继续下次循环,即执行redirect逻辑。

小结

HttpClient的RedirectStrategy定义了两个方法,一个是是否需要redirect,一个是获取redirect的请求,DefaultRedirectStrategy的构造器支持传入redirectMethods,默认是Get和Head,isRedirected方法主要是对302,301,307,303进行了判断,getRedirect方法主要是通过location获取目标地址,然后根据原来的method和statusCode构造HttpUriRequest。

0 人点赞