You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apache HttpClient重定向URL被自动解码,如何解决?

问题描述

使用Apache HttpClient 4.5.14发起请求时,收到302重定向响应,响应头Location包含URL编码的地址(例如%27单引号的编码),但HttpClient会自动解码该URL并请求解码后的地址,导致请求不符合预期。

现象日志

响应返回的Location头:

2023-10-31 07:25:38.899 DEBUG --- [ main] org.apache.http.headers : http-outgoing-2 << Location: https://[hostname-redacted]/temp/AB-C0244_BONNIE_BANKS_O_LOCH_LOMOND16987335293607/AB-C0244_TK22.2_BONNIE_BANKS_O%27_LOCH_LOMOND_General_Underscore_Abaco_Music_Library_(PRS).wav?Expires=1698733651&Signature=ZkxxNJhbLrX84ykFuFA7pKEhc05pCrUGJ8oQNjUbCswvh5o8K2k7W3HlG16zrX4EO2co4M1074X2uoHAlpTjnqhSz6FSnWeFud0LIEU6z99nvYmll6bgj1VQg~FqFz2aGLHF9qzRIeM2e73gWBU8bu5EVg5X5n-m8gdyz6-zg5M_&Key-Pair-Id=APKAIBH6A7AA3GUADXUQ

HttpClient实际请求的URL:

2023-10-31 07:25:38.902 DEBUG --- [ main] o.a.http.impl.execchain.MainClientExec : Executing request GET /temp/AB-C0244_BONNIE_BANKS_O_LOCH_LOMOND16987335293607/AB-C0244_TK22.2_BONNIE_BANKS_O'LOCH_LOMOND_General_Underscore_Abaco_Music_Library(PRS).wav?Expires=1698733651&Signature=ZkxxNJhbLrX84ykFuFA7pKEhc05pCrUGJ8oQNjUbCswvh5o8K2k7W3HlG16zrX4EO2co4M1074X2uoHAlpTjnqhSz6FSnWeFud0LIEU6z99nvYmll6bgj1VQg~FqFz2aGLHF9qzRIeM2e73gWBU8bu5EVg5X5n-m8gdyz6-zg5M_&Key-Pair-Id=APKAIBH6A7AA3GUADXUQ HTTP/1.1

问题根源

自定义RedirectStrategy无效,因为URL解码发生在RedirectStrategy传递URI之后。进一步排查发现,DefaultRedirectStrategy的getLocationURI方法中,当config.isNormalizeUri()为true时,会执行URIUtils.normalizeSyntax(uri)。按照URI标准化定义,该操作仅应解码非保留字符的百分编码,但实际却解码了%27(单引号属于保留字符),不符合预期。

复现代码

依赖配置(pom.xml)

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.14</version>
</dependency>

测试代码

开启org.apache.http调试日志后,运行以下代码可复现问题:当重定向到/test%27in时,HttpClient会请求/test'in:

package test;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class ApacheTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
        server.createContext("/test", new RedirectHttpHandler());
        server.createContext("/test'in", new SuccessHttpHandler());
        server.setExecutor(null);
        server.start();

        String uri = "http://127.0.0.1:8000/test";
        ApacheTest t = new ApacheTest();
        t.doRequest(uri);

        server.stop(0);
    }
    
    private CloseableHttpClient getClient() {
        RequestConfig requestConfig =
                RequestConfig.custom().setNormalizeUri(true).build();

        CloseableHttpClient httpClient =
                HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
        
        return httpClient;
    }
    
    private void doRequest(String uri) {
        try {
            CloseableHttpClient c = getClient();
            HttpGet get = new HttpGet(uri);
            CloseableHttpResponse response = c.execute(get);
            if (response.getStatusLine().getStatusCode() != 200) {
                throw new Exception("Expected 200 OK");
            }

            System.out.println("Got 200 OK!");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static class RedirectHttpHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange t) throws IOException {
            t.getResponseHeaders().add("Location", "/test%27in");
            t.sendResponseHeaders(302, 0);
            OutputStream os = t.getResponseBody();
            os.close();
        }
    }    
    
    public static class SuccessHttpHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange t) throws IOException {
            String response = "This is the response";
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }        
}
解决方案

针对该问题,有三种可行处理方式:

1. 关闭URI标准化

直接在RequestConfig中关闭URI标准化功能,这样URIUtils.normalizeSyntax不会执行,Location头的编码会被保留:

private CloseableHttpClient getClient() {
    RequestConfig requestConfig =
            RequestConfig.custom()
                    .setNormalizeUri(false) // 关闭URI标准化
                    .build();

    CloseableHttpClient httpClient =
            HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
    
    return httpClient;
}

注意:关闭标准化后,HttpClient不会自动处理URI中的冗余路径(如../),需自行确保Location头的URI格式正确。

2. 自定义RedirectStrategy并重写getLocationURI

如果需要保留部分标准化功能,同时避免解码保留字符,可以自定义RedirectStrategy,重写getLocationURI方法,跳过URIUtils.normalizeSyntax调用,或替换为符合预期的标准化逻辑:

import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.protocol.HttpContext;
import org.apache.http.HttpResponse;
import java.net.URI;

public class NoNormalizeRedirectStrategy extends DefaultRedirectStrategy {
    @Override
    protected URI getLocationURI(HttpResponse response, HttpContext context) throws org.apache.http.ProtocolException {
        HttpClientContext clientContext = HttpClientContext.adapt(context);
        RequestConfig config = clientContext.getRequestConfig();
        
        // 临时关闭标准化
        RequestConfig tempConfig = RequestConfig.copy(config)
                .setNormalizeUri(false)
                .build();
        clientContext.setRequestConfig(tempConfig);
        
        URI uri = super.getLocationURI(response, context);
        
        // 恢复原始配置
        clientContext.setRequestConfig(config);
        return uri;
    }
}

创建HttpClient时使用该自定义策略:

private CloseableHttpClient getClient() {
    RequestConfig requestConfig =
            RequestConfig.custom().setNormalizeUri(true).build();

    CloseableHttpClient httpClient =
            HttpClients.custom()
                    .setDefaultRequestConfig(requestConfig)
                    .setRedirectStrategy(new NoNormalizeRedirectStrategy()) // 应用自定义策略
                    .build();
    
    return httpClient;
}

3. 升级HttpClient版本

该问题在HttpClient后续版本(如5.x系列)中已被修复,若项目允许,升级到最新稳定版可直接解决URI标准化不符合预期的问题。


内容的提问来源于stack exchange,提问作者sbrattla

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 04:36:04