PAYATHON 2026

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 读取方面可能有不同表现。

可以按以下顺序排查:

  1. 检查当前 WebLogic、JDK/JRockit 和 CXF 的实际版本及兼容矩阵。
  2. 确认 WebLogic 是否配置了代理、连接池或自定义 SSL。
  3. 使用独立 Java 程序,在相同的 JVM/JDK 环境中直接调用该 URL。
  4. 在 WebLogic 容器内运行同样的测试,并比较结果。
  5. 如果只有容器内失败,重点检查 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,或没有正确结束响应。

支付调用的特别注意事项

发生读取超时时,不能直接认定支付请求失败。当前现象已经表明,网关可能完成了业务处理,只是客户端没有收到结果。直接重试可能造成重复授权或重复扣款。

更安全的处理方式是:

  1. 为每次请求分配唯一且可查询的业务流水号。
  2. 超时后先调用支付状态查询接口。
  3. 只有确认原交易未受理后,才能重新提交。
  4. 如果网关支持幂等键,应始终携带同一个幂等标识。
  5. 同时记录客户端流水号、网关流水号、请求时间和最终状态。

较有效的定位顺序是:先在同一服务器上用 curl 复现,再关闭 chunking 并核对超时配置,然后比较代理和 TLS 环境,最后通过抓包确认响应究竟停在网关、网络中间层,还是 WebLogic 的 HTTPS Handler。

备注:内容仅供参考。