国内直连
api.openai.com又慢又不稳,两个报错会先后冒出来。SocketTimeoutException: Read timed out是读超时,调超时或走流式能解决;Connection reset by peer是连接被中间设备悄悄 RST,加大超时没用,得换代理节点或加重试。最省心的是base-url指到稳定中转,再显式加大读写超时。
Issue
国内服务器用 Spring AI 调 OpenAI,经常在两个地方栽跟头:请求要么卡住抛读超时,要么中途断掉报连接重置。两者的现象都像「网络不好」,实际根因和修法完全不同。
最容易踩的时间点是:刚接入能跑通(短文本、快网络),一上国内生产环境或换成长生成,就间歇性报错。OpenAiApi 默认只传 api-key 和 base-url,其余全走默认,超时和代理都没显式配,问题就从这里冒出来。
Environment
- 网络:国内服务器,直连或经本地 Clash / v2ray 透明代理访问
api.openai.com,RTT(往返时延)偏高。 - 构建:Spring Boot 3.x + Spring AI(OpenAI 后端),JDK 17 / 21。
- 版本参考:issue #5594 环境为 Spring Boot 3.5.10、spring-ai-openai 1.1.2、Java 21、模型 gpt-5。
- 超时默认值:2.0.0-M5 起底层换成官方 openai-java SDK,
spring.ai.openai.timeout默认 60 秒;更早版本没有这个属性。
StackTrace
读超时,走 JDK HttpURLConnection 这条链路时抛,报错原文:
org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://api.openai.com/v1/chat/completions": Read timed out
完整堆栈,最内层是 JDK Socket 抛出的 SocketTimeoutException:
java.net.SocketTimeoutException: Read timed out
at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:283)
at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309)
at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:350)
at java.base/java.net.Socket$SocketInputStream.read(Socket.java:1091)
at org.springframework.http.client.SimpleStreamingClientHttpRequest.executeInternal(SimpleStreamingClientHttpRequest.java:137)
at org.springframework.web.client.RestClient$DefaultRequestBodyUriSpec.exchangeInternal(RestClient.java:400)
at com.example.demo.chat.OpenAiService.ask(OpenAiService.java:28)
at com.example.demo.chat.ChatController.ask(ChatController.java:22)
连接被重置,走 Reactor Netty(WebClient)这条链路时抛,报错原文:
org.springframework.web.reactive.function.client.WebClientRequestException: Connection reset
完整堆栈,最内层是 Netty 拿到的 SocketException: Connection reset:
reactor.core.Exceptions$ReactiveException: java.net.SocketException: Connection reset
at reactor.netty.channel.FluxReceive.onInboundError(FluxReceive.java:298)
at reactor.netty.channel.ChannelOperations.onInboundError(ChannelOperations.java:443)
at reactor.netty.http.client.HttpClientOperations.onInboundError(HttpClientOperations.java:588)
at reactor.netty.channel.ChannelOperationsHandler.exceptionCaught(ChannelOperationsHandler.java:158)
Caused by: java.net.SocketException: Connection reset
at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394)
at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:426)
Windows 上 JDK 会把后一个报成「远程主机强迫关闭了一个现有的连接」,本质是同一个东西。用 curl 直连时它显示为 Recv failure: Connection reset by peer。
Reproduce
最小依赖(当前版本官方 artifact 名,老版本为 spring-ai-openai-spring-boot-starter):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
不配代理、不调超时,最容易触发:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.openai.com/v1
一个最简调用:
@GetMapping("/ask")
String ask(@RequestParam String q) {
return chatClient.prompt().user(q).call().content();
}
这段代码本身不报错,报错取决于网络环境。要在本地确定性复现,先用 curl 打到网络层:
curl -v --max-time 15 https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY" 2>&1 | head -80
看到 * Connection reset by peer 或 curl: (56) Recv failure,就是链路被 RST;看到正常 HTTP 返回(哪怕是 401),说明网络通、问题在超时配置。
Root Cause
SocketTimeoutException:读超时被掐
SocketTimeoutException 是 JDK HttpURLConnection(SimpleClientHttpRequestFactory)抛的,不是 Netty。看到它,基本能判断走的是 JDK 客户端。
这条链路上有个容易误会的地方:readTimeout 不是「整个请求的总时长」,而是「两次数据到达之间的最大间隔」。所以即使总时长没超,只要服务端中间沉默太久——比如推理模型先想一阵才吐字——读超时照样触发。
国内再加一层:代理 RTT 高,长文本生成又动不动几十秒,一次请求很容易顶穿默认的 60 秒超时。
Connection reset:链路被 RST
这个和超时无关,加大超时没用。连接能建立、数据也传得动,但中途被一个 RST 包掐断,客户端就读到 Connection reset。国内网络下常见三个来源:
- TCP RST 注入:中间设备对流量发 RST 包强制断连,还伴生 DNS 污染把域名解析到错误 IP。
- 本地代理抖动:Clash、v2ray 节点不稳,或中转站主动断连。
- 上游主动关闭:空闲连接、超长连接被负载均衡或 CDN 回收。
判断的关键是看有没有 HTTP 状态码。真正的 429 会带 JSON body 和 Retry-After 头;Connection reset、读超时这类网络层失败,通常拿不到任何 HTTP 响应。
The Fix
第一步:curl 定位是哪一层
先别急着改代码,用上面的 curl 直连一次。见 reset 就换代理,见超时或挂起就调超时,两步别混着修。
第二步:配好代理或换中转
最省心的做法是 base-url 指到稳定中转(one-api、new-api,或自建 Cloudflare Worker):
spring:
ai:
openai:
base-url: https://your-relay.example.com/v1
api-key: ${OPENAI_API_KEY}
只想走本地透明代理(Clash 默认 7890 端口),用 SimpleClientHttpRequestFactory.setProxy:
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 7890)));
或者用 JVM 参数,JDK 的 HttpURLConnection 会读取:
java -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=7890 -jar app.jar
第三步:显式调大超时
新版直接配属性:
spring:
ai:
openai:
timeout: 120s
max-retries: 3
老版本没有 timeout 属性,就用自定义 RestClient.Builder,这条路所有版本都通吃:
@Bean
OpenAiApi openAiApi(@Value("${spring.ai.openai.api-key}") String apiKey,
@Value("${spring.ai.openai.base-url}") String baseUrl) {
RestClient.Builder restClientBuilder = RestClient.builder()
.requestFactory(ClientHttpRequestFactoryBuilder.simple()
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(120))
.build());
return OpenAiApi.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.restClientBuilder(restClientBuilder)
.build();
}
第四步:长生成走流式 + 重试
读超时和长生成是死对头。用 .stream() 代替 .call(),token 持续往外吐,两次数据之间不会沉默太久,读超时自然不容易触发。
Connection reset 大多是偶发抖动,配 max-retries 加退避就能扛住大部分。但别对超时无限重试,长生成重一遍更慢,用流式更实在。
References(参考来源)
- Spring AI OpenAI Chat — 官方文档spring-docs
- OpenAI request fails after 5 minutes despite increased timeouts — spring-ai issue #5594github
- [BUG] WebClientRequestException: Connection reset 偶发 — spring-ai-alibaba issue #3942github
- How to Fix AI API Failures That Look Like Rate Limits but Are Actually Network Issues — dev.todev.to
- Spring AI 实战:解决 Netty 超时问题 — 技术栈blog
