Apache CXF 调用 HTTPS WebService 时为何 Read timed out?
结论
这不像典型的证书校验失败,也不能只根据异常就认定 CXF “发送失败”。从调用栈判断,请求很可能已经到达支付网关,随后客户端在等待 HTTP 响应头或响应数据时超过了 receiveTimeout。
实际处理 HTTPS 的不是纯 JDK/CXF 实现,而是 WebLogic 的:
weblogic.net.http.SOAPHttpsURLConnection
排查时应重点关注以下几项:
- WebLogic HTTP/HTTPS Handler 与 CXF 是否存在兼容或配置问题;
- Java 进程与
curl使用的代理、DNS、TLS 或网络出口是否不同; - 网关能否正确处理 CXF 默认使用的 chunked request;
- 网关或中间设备是否已经处理请求,却未将响应正确送回当前连接;
receiveTimeout是否短于网关的实际处理时间。
增大超时时间只能处理响应确实较慢的情况,无法解决协议或网络路径不兼容。
从异常栈能判断什么
最内层异常是:
java.net.SocketTimeoutException: Read timed out
异常发生在:
SSLSocketImpl.readRecord
HttpClient.parseHTTP
HttpURLConnection.getResponseCode
HTTPConduit$WrappedOutputStream.handleResponse
这表明客户端已经进入读取阶段,正在等待 TLS 记录或 HTTP 响应头。如果问题出在证书不受信任、协议版本不匹配或握手失败,通常会出现 SSLHandshakeException、CertificateException 等异常,而不是单纯的 Read timed out。
Could not send Message 只是 CXF 对传输层异常的包装信息,并不代表请求一定没有发出。支付网关后台已经留下调用记录,也说明请求至少到达了网关或其前置系统。
先确认 curl 与 Java 的测试条件一致
应在运行 WebLogic/CXF 的同一台服务器上执行 curl,并尽量使用完全相同的请求体和请求头:
curl -v \
--connect-timeout 30 \
--max-time 120 \
-H 'Content-Type: text/xml; charset=UTF-8' \
-H 'SOAPAction: "authorize"' \
--data-binary @request.xml \
https://gateway.example.com/service
重点比较:
- 目标域名最终解析到的 IP;
- 是否经过 HTTP/HTTPS 代理;
- TLS 版本和加密套件;
- 客户端证书;
SOAPAction和Content-Type;Content-Length与Transfer-Encoding: chunked;- 请求体编码;
- 响应首字节到达时间。
本地电脑上的 curl 调用成功,并不能证明服务器中的 Java 进程网络正常。两者可能使用完全不同的 DNS、代理、防火墙和出口地址。
配置 CXF 的连接与读取超时
CXF 通常需要分别配置两类超时:
connectionTimeout:建立 TCP/TLS 连接时允许等待的时间;receiveTimeout:连接建立并发出请求后,等待响应数据的时间。
可以在创建客户端后配置 HTTPConduit:
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.transport.http.HTTPConduit;
PaymentService port = service.getPaymentServicePort();
Client client = ClientProxy.getClient(port);
HTTPConduit conduit = (HTTPConduit) client.getConduit();
HTTPClientPolicy policy = conduit.getClientSidePolicy();
if (policy == null) {
policy = new HTTPClientPolicy();
}
policy.setConnectionTimeout(30_000L);
policy.setReceiveTimeout(120_000L);
conduit.setClientSidePolicy(policy);
具体时间应根据网关的 SLA 设置。如果调大 receiveTimeout 后请求成功,说明网关响应时间或中间链路延迟超过了原有配置。如果每次都在固定时间超时,并且抓包显示完全没有响应,则更可能是网络、代理或协议兼容问题。
禁用 chunked request 进行验证
部分旧网关、反向代理或 Web Application Firewall 对 Transfer-Encoding: chunked 的支持不完整。它们可能已经建立交易记录,但仍在等待请求结束,导致客户端一直收不到响应。
可以临时关闭 CXF 的 chunking:
HTTPClientPolicy policy = conduit.getClientSidePolicy();
if (policy == null) {
policy = new HTTPClientPolicy();
}
policy.setAllowChunking(false);
policy.setConnectionTimeout(30_000L);
policy.setReceiveTimeout(120_000L);
conduit.setClientSidePolicy(policy);
如果关闭后恢复正常,应继续检查网关和中间代理对 chunked encoding 的支持,不能只把它当作超时问题。关闭 chunking 可能要求客户端先缓冲完整请求,因此超大请求还需要评估内存开销。
检查 WebLogic 的 URLConnection 实现
调用栈中出现了:
weblogic.net.http.HttpURLConnection
weblogic.net.http.SOAPHttpsURLConnection
这说明 WebLogic 替换了标准 URL Handler。某些 WebLogic、JDK/JRockit 和 CXF 组合在连接复用、代理、chunking 或 HTTPS 读取方面可能有不同表现。
可以按以下顺序排查:
- 检查当前 WebLogic、JDK/JRockit 和 CXF 的实际版本及兼容矩阵。
- 确认 WebLogic 是否配置了代理、连接池或自定义 SSL。
- 使用独立 Java 程序,在相同的 JVM/JDK 环境中直接调用该 URL。
- 在 WebLogic 容器内运行同样的测试,并比较结果。
- 如果只有容器内失败,重点检查 WebLogic Handler 和服务器网络配置。
部分 WebLogic 版本支持通过启动参数选择标准 JDK HTTP Handler,例如:
-DUseSunHttpHandler=true
该参数受版本和启动时机限制,不能在未经验证的情况下直接用于生产环境。应先查看对应 WebLogic 版本的文档,并在测试环境确认它不会影响其他 HTTP 客户端。
打开日志并抓取网络证据
CXF 日志可以用来确认 SOAP 消息和 HTTP 头是否符合预期。旧版 CXF 可以在 Bus 上加入日志拦截器:
import org.apache.cxf.Bus;
import org.apache.cxf.bus.spring.SpringBusFactory;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
Bus bus = new SpringBusFactory().createBus();
bus.getInInterceptors().add(new LoggingInInterceptor());
bus.getOutInterceptors().add(new LoggingOutInterceptor());
不同 CXF 版本的日志功能和类名可能不同,应以项目的实际依赖为准。日志中可能包含卡号、令牌、签名和个人信息,生产环境必须进行脱敏处理。
检查 TLS 时,可以临时启用:
-Djavax.net.debug=ssl,handshake
这类日志非常详细,也可能暴露证书和连接信息,只适合短时间诊断。
通常最有价值的证据是客户端和网关两端的抓包或连接日志。需要确认:
- TLS 握手是否完成;
- 请求是否完整发出;
- 服务端是否返回加密数据;
- 哪一端先发送 FIN 或 RST;
- 连接是否长时间没有数据;
- 响应是否被代理、防火墙或负载均衡器丢弃。
如果服务端声称已经返回响应,但客户端抓包没有收到任何响应包,应检查返回路由、中间防火墙、NAT、负载均衡器和代理。如果客户端已经收到数据,却仍在解析响应头时超时,则应检查响应是否不完整、HTTP 格式是否异常,以及连接是否正确结束。
还应检查的差异
如果问题仍然没有确定,应逐项比较 CXF 和 curl:
- Java 是否读取了
http.proxyHost、https.proxyHost等系统属性; curl是否通过环境变量使用代理,而 Java 没有使用,或情况相反;- 是否启用了双向 TLS,以及客户端证书是否一致;
- DNS 是否返回多个地址,其中部分节点存在异常;
- 是否使用 HTTP keep-alive,连接池中是否存在失效连接;
- 网关是否要求特定的
SOAPAction; - 请求使用的是 HTTP/1.0、HTTP/1.1,还是特定的 TLS 版本;
- 负载均衡器的 idle timeout 是否短于网关处理时间;
- 网关是否返回了错误的
Content-Length,或没有正确结束响应。
支付调用的特别注意事项
发生读取超时时,不能直接认定支付请求失败。当前现象已经表明,网关可能完成了业务处理,只是客户端没有收到结果。直接重试可能造成重复授权或重复扣款。
更安全的处理方式是:
- 为每次请求分配唯一且可查询的业务流水号。
- 超时后先调用支付状态查询接口。
- 只有确认原交易未受理后,才能重新提交。
- 如果网关支持幂等键,应始终携带同一个幂等标识。
- 同时记录客户端流水号、网关流水号、请求时间和最终状态。
较有效的定位顺序是:先在同一服务器上用 curl 复现,再关闭 chunking 并核对超时配置,然后比较代理和 TLS 环境,最后通过抓包确认响应究竟停在网关、网络中间层,还是 WebLogic 的 HTTPS Handler。
备注:内容仅供参考。
