-
Notifications
You must be signed in to change notification settings - Fork 40.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide a How-To for customizing WebClient's TcpClient #17856
Comments
Here's a single file that reproduces the problem: package com.example.demo;
import java.net.URI;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.ResponseSpec;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.TcpClient;
@SpringBootApplication
public class Gh17856Application {
public static void main(String[] args) {
SpringApplication.run(Gh17856Application.class, args);
}
@Bean
WebClient webClient(WebClient.Builder webClientBuilder) {
TcpClient tcpClient = TcpClient.create();
ClientHttpConnector connector = new ReactorClientHttpConnector(HttpClient.from(tcpClient));
return webClientBuilder.clientConnector(connector).build();
}
}
@RestController
class TestController {
private final WebClient webClient;
TestController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/test")
Mono<String> test() {
ResponseSpec result = webClient.method(HttpMethod.GET).uri(URI.create("https://postman-echo.com/get?a=42")).retrieve();
return result.bodyToMono(String.class);
}
} The |
This is looking like a Reactor Netty problem to me. Here's an attempt at reproducing the problem without Boot's involvement: package com.example.demo;
import java.net.URI;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.client.reactive.ReactorResourceFactory;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.TcpClient;
public class StandaloneReproduction {
public static void main(String[] args) {
perform();
perform();
}
private static void perform() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.afterPropertiesSet();
WebClient.Builder builder = WebClient.builder();
TcpClient tcpClient = TcpClient.create();
WebClient client = builder.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))).build();
System.out.println(client.method(HttpMethod.GET).uri(URI.create("https://postman-echo.com/get?a=42")).retrieve().bodyToMono(String.class).block());
factory.destroy();
}
} With Reactor Netty 0.8, the second
With 0.9, the failure is closer to the one reported above:
@violetagg I'm starting to get really out of my depth here. Can you please help me out and take a look? |
The example is not quite correct.
The code above creates the global HttpResources like the threads and the connection pool
Specifies
So at the end you are running with a connection pool from the global TcpResources, but with threads from the global HttpResources With the code below you destroy global HttpResources but not global TcpResources.
There are two possible solutions Create Tcp client with global HttpResources
Or destroy global TcpResources
|
Thanks very much, @violetagg. Applying the advice to the original problem and aligning with the recommendation in the reference docs to use a bean to customise the client connector, results in a bean definition like the following: @Bean
ClientHttpConnector clientHttpConnector(ReactorResourceFactory resourceFactory) {
TcpClient tcpClient = TcpClient.create(resourceFactory.getConnectionProvider())
.runOn(resourceFactory.getLoopResources())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 60000)
.doOnConnected((connection) -> connection.addHandlerLast(new ReadTimeoutHandler(60)));
return new ReactorClientHttpConnector(HttpClient.from(tcpClient));
} In this particular case a @Bean
WebClient webClient(WebClient.Builder builder) {
return builder.build();
} The documentation already mentions the |
One question: why do you create the HttpClient from a TcpClient and not directly with HttpClient.create()? The last will use by default the connection pool and the threads from the HttpResources and thus you do not need to specify them. |
I'd assumed that it was the only way to configure a connect timeout and to add a read timeout handler. If that's possible via |
you can do it like this
|
Thanks, @violetagg. I've just realised that one downside to the above is that it's not using the injected |
We're going to add a how-to in the reference docs showing how to customise the TCP client while sharing resources between WebClient and server. |
How can to do the unit testing and this methodo? |
Description:
Spring webflux internals break during devtools restart when WebClient bean is customized with a custom client connector.
spring boot version: Tested on 2.1.7.RELEASE and 2.2.0.M5 with a fresh kotlin start.spring.io project:
Only added 2 files to what was produced by start.spring.io:
Controller (to show failure case):
Configuration:
Replication Steps:
localhost:8080/test
. The first time, you will see the data come in.localhost:8080/test
again. This time, there will be a netty error:A temporary workaround is to not specify a custom client connector, but it'd be nice to be able to keep so we can hook in handlers, etc.
Thanks in advance!
The text was updated successfully, but these errors were encountered: