如何为 Spring WebClient 设置较长的读取超时(以避免 PrematureCloseException)

How to set a long read timeout for Spring WebClient (to avoid PrematureCloseException)

提问人:THelper 提问时间:11/16/2023 最后编辑:THelper 更新时间:11/18/2023 访问量:13

问:

我有一个带有 Spring WebClient 的 Spring Boot 应用程序向另一个 Spring-Boot 应用程序(Spring-Boot 2.7.14 和 Spring WebFlux 5.3.29)发送请求

    WebClient.Builder wcBuilder = WebClient.builder()
            .exchangeStrategies(ExchangeStrategies.builder()
                    .codecs(configurer -> configurer
                            .defaultCodecs()
                            .maxInMemorySize(MAX_RESPONSE_SIZE_MB * BYTES_PER_MB))
                    .build());

    WebClient webClient = wcBuilder
            .baseUrl(url)
            .defaultHeader(HttpHeaders.USER_AGENT, "MyClient v1.0")
            .build();                        
                    
    webClient.post()
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .bodyValue(someData)
            .retrieve()
            .bodyToMono(MyClass.class);  

有些请求需要大量处理,这些请求可能需要大约 60 - 90 秒才能完成。我的问题是这些请求总是返回错误

org.springframework.web.reactive.function.client.WebClientRequestException: 
Connection prematurely closed BEFORE response; nested exception is reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response

50-51 秒后。为了解决这个问题,我尝试按照此处的建议添加一个并设置超时ReactorClientHttpConnector

    WebClient.Builder wcBuilder = WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(HttpClient.create()
                    .responseTimeout(Duration.ofSeconds(TIMEOUT))))
    ...
                    

将超时设置为 40 秒时,我在 40-41 秒 () 后出现略有不同的错误,因此该设置有效。但是,当我将超时设置为 90 或 120 秒时,我仍然会在 50 秒后收到 PrematureCloseException。我查看了我可能缺少的其他设置,并尝试使用 ConnectionProvider 和各种其他选项,例如io.netty.handler.timeout.ReadTimeoutException

ConnectionProvider provider =
            ConnectionProvider.builder("custom")
                    .maxConnections(100)
                    .maxIdleTime(Duration.ofSeconds(TIMEOUT))
                    .maxLifeTime(Duration.ofSeconds(TIMEOUT)).build();

    WebClient.Builder wcBuilder = WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(HttpClient.create(provider)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .option(EpollChannelOption.TCP_KEEPIDLE, 300)
                    .option(EpollChannelOption.TCP_KEEPINTVL, 60)
                    .option(EpollChannelOption.TCP_KEEPCNT, 8)
                    .doOnConnected(conn -> conn
                            .addHandler(new ReadTimeoutHandler(TIMEOUT, TimeUnit.SECONDS)))
                    .responseTimeout(Duration.ofSeconds(TIMEOUT))))
    ...
                    

但结果都是一样的。我在这里缺少什么设置?

我认为问题出在客户端,因为我之前在与不同的(基于 Python)服务器交谈时遇到了同样的问题。此外,我在服务器端没有看到任何错误,服务器始终正常完成处理。

附录:我看到的是wiretap(true)

15:44:31.670 [reactor-http-epoll-2] DEBUG reactor.netty.http.client.HttpClient:145 - [...] READ COMPLETE
15:44:31.682 [reactor-http-epoll-2] DEBUG reactor.netty.resources.PooledConnectionProvider:259 - [...] Channel closed, now: 0 active connections, 0 inactive connections and 0 pending acquire requests.
15:44:31.683 [reactor-http-epoll-2] DEBUG reactor.netty.ReactorNetty:259 - [] Non Removed handler: ReadTimeoutHandler, context: ChannelHandlerContext(ReadTimeoutHandler, [...]), pipeline: DefaultChannelPipeline{(reactor.left.sslHandler = io.netty.handler.ssl.SslHandler), (reactor.left.loggingHandler = reactor.netty.transport.logging.ReactorNettyLoggingHandler), (reactor.left.httpCodec = io.netty.handler.codec.http.HttpClientCodec), (reactor.left.responseTimeoutHandler = io.netty.handler.timeout.ReadTimeoutHandler), (ReadTimeoutHandler = io.netty.handler.timeout.ReadTimeoutHandler), (reactor.right.reactiveBridge = reactor.netty.channel.ChannelOperationsHandler)}
15:44:31.683 [reactor-http-epoll-2] DEBUG reactor.netty.ReactorNetty:259 - [...] Non Removed handler: reactor.left.responseTimeoutHandler, context: ChannelHandlerContext(reactor.left.responseTimeoutHandler, [...]), pipeline: DefaultChannelPipeline{(reactor.left.sslHandler = io.netty.handler.ssl.SslHandler), (reactor.left.loggingHandler = reactor.netty.transport.logging.ReactorNettyLoggingHandler), (reactor.left.httpCodec = io.netty.handler.codec.http.HttpClientCodec), (reactor.left.responseTimeoutHandler = io.netty.handler.timeout.ReadTimeoutHandler), (ReadTimeoutHandler = io.netty.handler.timeout.ReadTimeoutHandler), (reactor.right.reactiveBridge = reactor.netty.channel.ChannelOperationsHandler)}
15:44:31.686 [reactor-http-epoll-2] DEBUG reactor.netty.http.client.HttpClient:145 - [...] USER_EVENT: SslCloseCompletionEvent(java.nio.channels.ClosedChannelException)
15:44:31.688 [reactor-http-epoll-2] DEBUG reactor.netty.http.client.HttpClient:145 - [...] INACTIVE
15:44:31.688 [reactor-http-epoll-2] DEBUG reactor.netty.resources.DefaultPooledConnectionProvider:259 - [...] onStateChange(POST{uri=..., connection=PooledConnection{channel=[...]}}, [response_incomplete])
15:44:31.694 [reactor-http-epoll-2] WARN reactor.netty.http.client.HttpClientConnect:304 - [...] The connection observed an error
reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response
15:44:31.716 [reactor-http-epoll-2] DEBUG reactor.netty.http.client.HttpClient:145 - [...] UNREGISTERED
15:44:32.216 [Connection evictor] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager:448 - Closing expired connections
15:44:32.216 [Connection evictor] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager:441 - Closing connections idle longer than 50000 MILLISECONDS

因此,有一个 Apache PoolingHttpClientConnectionManager 会在 50 秒后终止连接。但从日志来看,似乎是 Netty 正在关闭 在此之前连接?

java spring-boot 异常 netty spring-webclient

评论


答: 暂无答案