WebClient encoding queryParams spring - spring

I have trouble with WebClient encoding query parameters when the value of one parameter is decoded JSON value to String.
One of queryParams value is :
[ { "var": "report_days", "op": "=", "val": "7" } ]
it is decoded from HTTP method : ?filter=%5B%7B%22var%22%3A%22report_days%22%2C%22op%22%3A%22%3D%22%2C%22val%22%3A%227%22%7D%5D.
So decoding to MultiMap<String, String> is executed correctly, but in uriBuilder the exception is thrown.
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("/nodes/last").queryParams(queryParams).build()) //Problem
.header(HttpHeaders.AUTHORIZATION, token)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.log();
Exception:
java.lang.IllegalArgumentException: Not enough variable values available to expand '"var"'
2021-11-22T11:17:38.252421700Z at org.springframework.web.util.UriComponents$VarArgsTemplateVariables.getValue(UriComponents.java:370)
2021-11-22T11:17:38.252461800Z Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
2021-11-22T11:17:38.252492300Z Error has been observed at the following site(s):
2021-11-22T11:17:38.252521200Z *__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
2021-11-22T11:17:38.252586100Z *__checkpoint ⇢ HTTP GET "/nodeNew/all/last_protected?filter=%5B%7B%22var%22%3A%22report_days%22%2C%22op%22%3A%22%3D%22%2C%22val%22%3A%227%22%7D%5D" [ExceptionHandlingWebHandler]
2021-11-22T11:17:38.252628200Z Stack trace:
2021-11-22T11:17:38.252666300Z at org.springframework.web.util.UriComponents$VarArgsTemplateVariables.getValue(UriComponents.java:370)
2021-11-22T11:17:38.252699800Z at org.springframework.web.util.HierarchicalUriComponents$QueryUriTemplateVariables.getValue(HierarchicalUriComponents.java:1087)
2021-11-22T11:17:38.252723100Z at org.springframework.web.util.UriComponents.expandUriComponent(UriComponents.java:263)
2021-11-22T11:17:38.252738600Z at org.springframework.web.util.HierarchicalUriComponents.lambda$expandQueryParams$5(HierarchicalUriComponents.java:450)
2021-11-22T11:17:38.252754400Z at java.base/java.util.Map.forEach(Map.java:713)
Maybe is some of configuration to solve it? In queryParams might be another values but not in JSON format, so I would like to avoid do it in that way (that works now, but it have to forward all queryParams not only key "filter"):
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("/nodes/last").queryParam(URLEncoder.encode(queryParams.getFirst("filter"), StandardCharsets.UTF_8)).build())

I came across with the same trouble recently and this did the job:
Create a method that returns a copy of webclient with a custom DefaultUriBuilderFactory
public WebClient getWebclientNoEncoded() {
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(this.baseUrl); //Here comes your base url
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
return this.webClient.mutate()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.uriBuilderFactory(factory)
.build();
}
And then in the client method:
apiClient.getWebclientNoEncoded()
.get()
.uri(uriBuilder -> uriBuilder
.path("/foo")
.queryParam(UriUtils.encodeQueryParam(myJsonString, StandardCharsets.UTF_8.toString()))
.build())
.header(HttpHeaders.AUTHORIZATION, bearerToken)
.retrieve()
PD. Sorry about my poor english.

Related

Unable to save data received by Spring Boot WebCliet

The things I want to do is: to get data from https://jsonplaceholder.typicode.com/ and save those data into my machine. I want to save the posts from this site. I want to do it by Spring Boot WebClient. I have followed several tutorials, articles, and also WebClient documentation. But Unable to save the response in my local database.
The below URL will return one post.
https://jsonplaceholder.typicode.com/posts/1
If I want to return the post as the response of another API it is working fine, but not able to use the inside program. I have tried with WebClient .block(), but it is working for standalone applications but not for web application.
GitLab link of the project
Controller :
#Autowired
private PostService postService;
// working fine.
#GetMapping(value = "posts", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
#ResponseStatus(HttpStatus.OK)
public Flux<Post> findAll() {
return postService.findAll();
}
#GetMapping(value = "postsSave")
#ResponseStatus(HttpStatus.OK)
public String saveAll() {
return postService.saveAll();
}
Service:
#Override
public String saveAll() {
// Post posts = webClient.get()
// .uri("/posts")
// .retrieve()
// .bodyToFlux(Post.class)
// .timeout(Duration.ofMillis(10_000)).blockFirst();
String url = "https://jsonplaceholder.typicode.com/posts/1";
WebClient.Builder builder = WebClient.builder();
Post p = builder.build()
.get()
.uri(url)
.retrieve()
.bodyToMono(Post.class)
.block(); // this line generating error.
postRepository.save(p);
return "saved";
}
Exception StackTrace:
2022-12-07 14:35:44.070 ERROR 6576 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [b48b7f19-1] 500 Server Error for HTTP GET "/postsSave"
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ HTTP GET "/postsSave" [ExceptionHandlingWebHandler]
Stack trace:
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
at reactor.core.publisher.Mono.block(Mono.java:1680) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
at com.quantsys.service.PostService.saveAll(PostService.java:53) ~[classes/:na]
at com.quantsys.controller.PostController.saveAll(PostController.java:26) ~[classes/:an]
But the same code of snippet is working within the Bootstrap class:
#SpringBootApplication
public class QuanrsysPostService {
public static void main(String[] args) {
SpringApplication.run(QuanrsysPostService.class, args);
String url = "https://jsonplaceholder.typicode.com/posts/1";
WebClient.Builder builder = WebClient.builder();
Post p = builder.build()
.get()
.uri(url)
.retrieve()
.bodyToMono(Post.class)
.block(); // working here.
System.out.println(p.toString());
}
}

Cannot test WebClient exception situation

I use Spring WebClient and write a test by following this page.
Everything is ok except from the exception test. I tried to use many different exception classes, but each time I get:
java.lang.AssertionError: Expecting a throwable with cause being an instance >of:java.lang.RuntimeException but current throwable has no cause.
The test method is here:
#Test
void throwsProductServiceExceptionWhenErrorStatus() throws JsonProcessingException {
server.enqueue(
new MockResponse()
.setResponseCode(500)
.setHeader("content-type", "application/json")
.setBody("{}"));
assertThatThrownBy(() -> client.findByName("flower"))
.hasCauseInstanceOf(RuntimeException.class);
// I also tried many other exception types e.g. JsonProcessingException
}
Here is my WebClient method that I want to create exception test:
private String fetchData(String title, String uri) {
// make GET call with WebClient.get and use a Mono of String to get the response as JSON string
return webClient
.get()
.uri(uri, title)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is5xxServerError,
error -> Mono.error(new RuntimeException(ERROR_WHEN_FETCHING_DATA)))
.bodyToMono(String.class)
.block();
}
Any idea?

Call REST endpoint using webclient which has both PathVariable and RequestParam

I have the following endpoint, which needs to be called from webclient. date is red using #PathVariable and name is red using #RequestParam annotations.
/api/v1/names/officialNameByDate/{date}?name=empName
Is the following usage correct?
String response = webclient.get()
.uri(uriBuilder -> uriBuilder
.path("/api/v1/names/officialNameByDate/{date}")
.queryParam("name", empName)
.build(date))
.retrieve()
.bodyToMono(String.class).block();
Yes, you can use both Path variable and Request Param.
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/{id}/attributes/{attributeId}")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.build(2, 13))
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/2/attributes/13?color=black");
See above code as an example.

How to set base url and query parameters for WebClient?

In my service, I have to get response from some different urls with parameters.
get from http://a.com:8080/path1?param1=v1
get from http://b.com:8080/path2?param2=v2
get from http://c.com:8080/path3?param3=v3
I am using WebClient to do the job as following.
public class WebClientTest {
private WebClient webClient = WebClient.builder().build();
#Test
public void webClientTest() {
Mono<String> a = webClient.get()
.uri(uriBuilder -> uriBuilder.scheme("http").host("a.com").port(8080).path("/path1")
.queryParam("param1", "v1")
.build())
.retrieve()
.bodyToMono(String.class);
Mono<String> b = webClient.get()
.uri(uriBuilder -> uriBuilder.scheme("http").host("b.com").port(8080).path("/path2")
.queryParam("param2", "v2")
.build())
.retrieve()
.bodyToMono(String.class);
Mono<String> c = webClient.get()
.uri(uriBuilder -> uriBuilder.scheme("http").host("c.com").port(8080).path("/path3")
.queryParam("param3", "v3")
.build())
.retrieve()
.bodyToMono(String.class);
//zip the result
}
}
As you can see, I have to set scheme, host, port separately again and again.
So my questions are:
1. Am I using WebClient in a right way?
2. Is it possible to set scheme, host, port in a method together? I know that webClient.get().uri("http://a.com:8080/path1?param1=v1").retrieve() works, but what I am expecting is something like:
webClient.get()
.uri(uriBuilder -> uriBuilder/*.url("http://a.com:8080/path1")*/
.queryParam("param1", "v1")
.build())
.retrieve()
.bodyToMono(String.class);
As of Spring Framework 5.2, there is an additional method that can help with your specific situation:
Mono<String> response = this.webClient
.get()
.uri("http://a.com:8080/path1", uri -> uri.queryParam("param1", "v1").build())
.retrieve()
.bodyToMono(String.class);
I wouldn't advise creating one WebClient per host as a general rule. It really depends on your use case. Here it seems your client might send requests to many hosts, and creating many HTTP clients can be a bit wasteful here.
The way I solved this was to have a WebClient for each different url.
So you would have
private WebClient aClient = WebClient.create("a.com")
private WebClient bClient = WebClient.create("b.com")
private WebClient cClient = WebClient.create("c.com")
Then interact with each WebClient depending on what you're calling.
https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web-reactive.html#webflux-client-retrieve

How to handle HTTP status code in Spring Webclient

I'm stuck trying to do simple error handling when calling a remote service. The service returns a Map. The behaviour I'm looking for is:
HTTP 200 --> Return body (Map<String, String>).
HTTP 500 --> Throw a particular exception
HTTP 404 --> Simply return Null.
Here's my code:
private Map<String, String> loadTranslations(String languageTag) {
try {
WebClient webClient = WebClient.create(serviceUrl);
Map<String, String> result = webClient.get()
.uri("/translations/{language}", languageTag)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(httpStatus -> HttpStatus.NOT_FOUND.equals(httpStatus),
clientResponse -> Mono.error(new MyServiceException(HttpStatus.NOT_FOUND)))
.onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new MyServiceException(response.statusCode())))
.bodyToMono(Map.class)
.block();
return result;
} catch (MyServiceException ex) { // doesn't work as in reality it throws ReactiveException
....
}
}
I don't know how to have the result of block() return NULL (or something that I can interpret as "404 was received"). The idea would be to just return NULL on 404 and throw an exception on 500.
I tried returning Mono.empty() but in that case the result variable contains the body of the response as Dictionary (I'm using standard Spring error bodies that contain timestamp, path, message).
What I'm doing wrong?
Thank you,

Resources