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

Jersey客户端接收分块gzip响应时无法复用连接的问题

分块Gzip响应下Jersey ApacheConnector连接无法复用的解决方法

在使用Jersey 3.x/2.x搭配ApacheConnector客户端时,若服务端返回分块编码的Gzip压缩响应,会出现连接读取响应后立即关闭、无法复用的问题;但非Gzip压缩的分块响应可正常复用连接。问题触发点在ApacheConnectionClosingStrategy类的request.abort()逻辑,即便配置了连接池、长连接及复用策略也无法解决。

问题原因

Jersey的ApacheConnector默认关闭策略在处理Gzip压缩的分块响应时,会误判连接状态为未释放,从而触发abort()强制关闭连接。这是因为Gzip压缩流的包装导致连接复用的判断逻辑失效,而普通分块响应不存在这个包装层,所以可正常复用。

解决方法

方法1:自定义连接关闭策略

重写默认的ApacheConnectionClosingStrategy,针对Gzip分块响应跳过abort()操作,改为正常释放连接:

import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.util.EntityUtils;
import org.glassfish.jersey.apache.connector.ApacheClientRequest;
import org.glassfish.jersey.apache.connector.ApacheConnectionClosingStrategy;

import java.io.IOException;

public class GzipChunkedConnectionClosingStrategy implements ApacheConnectionClosingStrategy {
    @Override
    public void close(ApacheClientRequest request, boolean forceClose) {
        HttpResponse response = request.getHttpResponse();
        if (response == null) {
            if (request.getHttpRequest() != null) {
                request.getHttpRequest().abort();
            }
            return;
        }

        // 判断是否为Gzip分块响应
        Header contentEncoding = response.getFirstHeader(HttpHeaders.CONTENT_ENCODING);
        Header transferEncoding = response.getFirstHeader(HttpHeaders.TRANSFER_ENCODING);
        boolean isGzipChunked = contentEncoding != null && "gzip".equalsIgnoreCase(contentEncoding.getValue())
                && transferEncoding != null && "chunked".equalsIgnoreCase(transferEncoding.getValue());

        if (forceClose || !isGzipChunked) {
            // 非目标场景或强制关闭时,执行原abort逻辑
            HttpRequestBase httpRequest = request.getHttpRequest();
            if (httpRequest != null) {
                httpRequest.abort();
            }
        } else {
            // Gzip分块响应:确保实体被释放,而非强制abort
            try {
                EntityUtils.consume(response.getEntity());
            } catch (IOException ignored) {
                // 忽略释放时的异常
            }
        }
    }
}

然后在客户端配置中替换默认策略:

// 原连接池配置不变
PoolingHttpClientConnectionManager connectionManager2 = new PoolingHttpClientConnectionManager();
connectionManager2.setMaxTotal(100);
connectionManager2.setDefaultMaxPerRoute(100);

final ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(new ApacheConnectorProvider());
clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager2);
clientConfig.property(ApacheClientProperties.KEEPALIVE_STRATEGY,
        (ConnectionKeepAliveStrategy) (response, context) -> 60000);
clientConfig.property(ApacheClientProperties.REUSE_STRATEGY,
        (ConnectionReuseStrategy) (response, context) -> true);
// 添加自定义关闭策略
clientConfig.property(ApacheClientProperties.CONNECTION_CLOSING_STRATEGY, 
        new GzipChunkedConnectionClosingStrategy());

JERSEY_CLIENT = ClientBuilder.newClient(clientConfig);

方法2:让Apache HttpClient自动处理Gzip压缩

绕过Jersey的压缩处理逻辑,让Apache HttpClient在底层自动完成Gzip解压,使Jersey处理的是普通分块响应:

import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.ConnectionReuseStrategy;
import org.apache.http.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestInterceptor;
import org.apache.http.protocol.HttpResponseInterceptor;
import org.apache.http.client.entity.GzipDecompressingEntity;

PoolingHttpClientConnectionManager connectionManager2 = new PoolingHttpClientConnectionManager();
connectionManager2.setMaxTotal(100);
connectionManager2.setDefaultMaxPerRoute(100);

// 配置HttpClient自动处理Gzip
HttpClient httpClient = HttpClientBuilder.create()
        .setConnectionManager(connectionManager2)
        .setKeepAliveStrategy((response, context) -> 60000)
        .setConnectionReuseStrategy((response, context) -> true)
        // 添加请求头:接受Gzip压缩
        .addInterceptorFirst((HttpRequestInterceptor) (request, context) -> {
            if (!request.containsHeader(HttpHeaders.ACCEPT_ENCODING)) {
                request.addHeader(new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "gzip"));
            }
        })
        // 响应拦截:自动解压Gzip实体
        .addInterceptorFirst((HttpResponseInterceptor) (response, context) -> {
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                Header encoding = entity.getContentEncoding();
                if (encoding != null && "gzip".equalsIgnoreCase(encoding.getValue())) {
                    response.setEntity(new GzipDecompressingEntity(entity));
                }
            }
        })
        .build();

final ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(new ApacheConnectorProvider());
// 直接传入配置好的HttpClient
clientConfig.property(ApacheClientProperties.HTTP_CLIENT, httpClient);

JERSEY_CLIENT = ClientBuilder.newClient(clientConfig);

方法3:确保实体被完全消费

若使用InputStream读取响应,需确保流被完全读取并关闭,避免连接因未消费完实体被判定为不可复用:

import org.apache.commons.io.IOUtils;

// 读取响应时完全消费流
try (InputStream is = response.readEntity(InputStream.class)) {
    IOUtils.copy(is, new ByteArrayOutputStream()); // 将流读取到缓冲区,确保完全消费
} catch (IOException e) {
    // 按需处理异常
}

此方法需配合其他方案使用,单独使用可能无法彻底解决问题,但能减少异常场景。

Jersey本身并非不支持分块Gzip响应,只是ApacheConnector的默认策略在该场景下存在逻辑缺陷,通过上述方法可修复连接复用问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 15:05:57