How to validate response object in Spring WebClient - spring-boot

I am using Springs' Webclient to make a HTTP GET call.
How can I validate the response object GetPersonBasicInfoResWrapper's property that I received as a response of my HTTP call.
I am trying to validate the birthDate inside the flatMap by blocking the response object, but it doesn't look like the most functional way of doing it.
Following is the excerpt from my code.
private Mono<GetPersonBasicInfoResWrapper> getPersonBasicInfo(Double personId, LocalDate birthDate,
CallerRequestMetaData callerInfo) {
return middlewareWebClient
.get()
.uri(...)
...
...
.exchange()
.flatMap(client -> {
GetPersonBasicInfoResWrapper block = client.bodyToMono(GetPersonBasicInfoResWrapper.class).block();
LocalDate personBirthDate = LocalDateTime.ofInstant(block.getBirthDate().toInstant(),ZoneId.of(Constants.DEFAULT_TIME_ZOME)).toLocalDate();
if (!personBirthDate.equals(birthDate))
throw new YakeenRowadException(Errors.INCORRECT_ID_BIRTH_DATE_G, birthDate.toString());
else
return client.bodyToMono(GetPersonBasicInfoResWrapper.class);
});
}
Any help is highly appreciated.

Try something like this
private Mono<PersonInfo> getPersonInfo(Double personId) {
return webClient.get()
.uri(...)
.exchange()
.flatMap(response -> {
return response.bodyToMono(PersonInfo.class);
});
}
private LocalDate toLocalDate(Instant instant) {
return LocalDateTime.ofInstant(instant, ZoneId.of(Constants.DEFAULT_TIME_ZOME))
.toLocalDate();
}
public Mono<PersonInfo> doSomething(Double personId, LocalDate birthDate) {
return getPersonInfo(personId)
.flatMap(personInfo -> {
final LocalDate birthDate = toLocalDate(personInfo.getBirthDate().toInstant());
if (!personBirthDate.equals(birthDate)) {
return Mono.error(new YakeenRowadException(Errors.INCORRECT_ID_BIRTH_DATE_G, birthDate.toString()));
}
return Mono.just(personInfo);
});
}
Don't validate during the fetch, validation is business logic, and should be in the layer above.
you fetch and return
you validate.
If validation fails, you return a Mono.error() to the calling client.
I have no idea what "MetaData" was supposed to be. I hope it's not the url, because passing it that way is wrong.
(try to avoid verbose naming)

Related

Spring WebClient Post method Body

i'm trying to send a POST request with body data as described here: https://scrapyrt.readthedocs.io/en/stable/api.html#post.
Here's what i've tried to do but it gives me HTTP code 500
String uri = "http://localhost:3000";
WebClient webClient = WebClient.builder()
.baseUrl(uri)
.build();
LinkedMultiValueMap map = new LinkedMultiValueMap();
String q = "\"url\": \"https://blog.trendmicro.com/trendlabs-security-intelligence\",\"meta\":{\"latestDate\" : \"18-05-2020\"}}";
map.add("request", q);
map.add("spider_name", "blog");
BodyInserter<MultiValueMap<String, Object>, ClientHttpRequest> inserter2
= BodyInserters.fromMultipartData(map);
Mono<ItemsList> result = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/crawl.json")
.build())
.body(inserter2)
.retrieve()
.bodyToMono(ItemsList.class);
ItemsList tempItems = result.block();
Here's what i've tried to do but it gives me HTTP code 500
Most likely because you're sending the wrong data in a mixture of wrong formats with the wrong type:
You're using multipart form data, not JSON
You're then setting the request parameter as a JSON string (q)
The JSON string you're using in q isn't even valid (it's at least missing an opening curly brace) - and handwriting JSON is almost universally a bad idea, leverage a framework to do it for you instead.
Instead, the normal thing to do would be to create a POJO structure that maps to your request, so:
public class CrawlRequest {
private CrawlInnerRequest request;
#JsonProperty("spider_name")
private String spiderName;
//....add the getters / setters
}
public class CrawlInnerRequest {
private String url;
private String callback;
#JsonProperty("dont_filter")
private String dontFilter;
//....add the getters / setters
}
...then simply create a CrawlRequest, set the values as you wish, then in your post call use:
.body(BodyInserters.fromValue(crawlRequest))
This is a rather fundamental, basic part of using a WebClient. I'd suggest reading around more widely to give yourself a better understanding of the fundamentals, it will help tremendously in the long run.
For me following code worked:
public String wcPost(){
Map<String, String> bodyMap = new HashMap();
bodyMap.put("key1","value1");
WebClient client = WebClient.builder()
.baseUrl("domainURL")
.build();
String responseSpec = client.post()
.uri("URI")
.headers(h -> h.setBearerAuth("token if any"))
.body(BodyInserters.fromValue(bodyMap))
.exchange()
.flatMap(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError()) {
clientResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
return clientResponse.bodyToMono(String.class);
}
else
return clientResponse.bodyToMono(String.class);
})
.block();
return responseSpec;
}

How use thenEmpty and then thenReturn in Spring WebFlux?

I'm new in Spring WebFlux and I have a problem. I want to return something like Mono<String> as follow:
#PostMapping("/test")
public Mono<String> test(){
return Mono.just("Test String")
.thenEmpty(it -> {
// Do something I need
System.out.println("Print somethings");
})
.thenReturn("Return String");
}
I wish the method to return Return String, But it return nothing. What's the Problem?
thenEmpty will be invoked and return Mono<Void> as part of pipeline order. since Mono<Void> return, rest of the operator in pipeline is not working. if you want to use thenReturn then use below code.
#PostMapping("/test")
public Mono<String> test(){
return Mono.just("Test String")
.doOnNext(s -> {
// do what you want
})
.thenReturn("Return String");
}
you can try something like this:
#PostMapping("/test") public Mono<String> test(){
return Mono.just("Test String")
.doOnNext(s -> {
// do what you want
})
.map(s -> {
return "done";
});
}
You can use other doOn* methods, depending on what you need. For example doOnSuccess or doOnError.
Then you can use map if you need to manipulate your data (but keeping return type).

Spring webflux "Only one connection receive subscriber allowed" if return server response from switchIfEmpty

I would like to put a case where if object exist then send error if not then create new user.
here is my handler:
public Mono<ServerResponse> createUser(ServerRequest request) {
Mono<UserBO> userBOMono = request.bodyToMono(UserBO.class);
Mono<String> email = userBOMono.map(UserBO::getEmail);
Mono<User> userMono = email.flatMap(userRepository::findByEmail);
return userMono.flatMap(user -> {
Mono<ErrorResponse> errorResponseMono = errorHanlder.handleEmailAlreadyExist();
return ServerResponse.status(HttpStatus.CONFLICT)
.contentType(MediaType.APPLICATION_JSON)
.body(errorResponseMono, ErrorResponse.class);
}).switchIfEmpty(Mono.defer(() -> {
Mono<User> newUserMono = userBOMono.flatMap(userMapping::mapUserBOToUser);
Mono<User> dbUserMono = newUserMono.flatMap(userRepository::save);
return ServerResponse.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.body(dbUserMono, User.class);
}));
if Mono is not empty then its return conflict that what I want if if empty then create new but its throwing below error:
java.lang.IllegalStateException: Only one connection receive subscriber allowed.
at reactor.ipc.netty.channel.FluxReceive.startReceiver(FluxReceive.java:276) ~[reactor-netty-0.7.8.RELEASE.jar:0.7.8.RELEASE]
at reactor.ipc.netty.channel.FluxReceive.lambda$subscribe$2(FluxReceive.java:127) ~[reactor-netty-0.7.8.RELEASE.jar:0.7.8.RELEASE]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:163) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:404) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:464) ~[netty-transport-4.1.27.Final.jar:4.1.27.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_131]
Update Note: its correct behavior as per method definition:
switchIfEmpty(Mono<? extends T> alternate)
Fallback to an alternative Mono if this mono is completed without data
Means when I am sending empty Mono in body its work fine:
return ServerResponse.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.empty(), User.class);
so what is solution to handle swtichIfEmpty case if I would like to send Mono object as return from it.
Finally I was able to resolve it, I was reading userBOMono stream twice which was causing this error to throw by webflux.
so here is updated code which works fine.
public Mono<ServerResponse> createUser(ServerRequest request) {
Mono<UserBO> userBOMono = request.bodyToMono(UserBO.class);
return userBOMono.flatMap(userBO -> {
String email = userBO.getEmail();
Mono<User> userMono = userRepository.findByEmail(email);
return userMono.flatMap(user -> errorHandler.handleEmailAlreadyExist())
.switchIfEmpty(Mono.defer(() -> createNewUser(userBO)));
});
}
private Mono<ServerResponse> createNewUser(UserBO userBO) {
Mono<User> userMono = Mono.just(userBO).flatMap(userMapping::mapUserBOToUser).flatMap(userRepository::save);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body(userMono, User.class);
}
I assume you use a WebClient to invoke this API.
The client should not subscribe more than once, otherwise this error can come.
I've got the same error by running my #SpringBootTest class.
The problem seems to be that response was being writed while methods had already been closed.
Solved by passing "Mono.empty()" instead of full response.
Code Before:
WebClient.create()
.get()
.uri(new URI(UPDATE_COMPANIES_URL))
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Boolean.class).thenReturn(Boolean.TRUE);
} else {
System.out.println("[sendSecureRequest] Error sending request: " + response.statusCode());
return response.bodyToMono(Boolean.class).thenReturn(Boolean.FALSE);
}
}).subscribe();
Code After:
WebClient.create()
.get()
.uri(new URI(UPDATE_COMPANIES_URL))
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
// TODO handle success
} else {
System.out.println("[sendSecureRequest] Error sending request: " + response.statusCode());
}
return Mono.empty();
}).subscribe();

Get API response error message using Web Client Mono in Spring Boot

I am using webflux Mono (in Spring boot 5) to consume an external API. I am able to get data well when the API response status code is 200, but when the API returns an error I am not able to retrieve the error message from the API. Spring webclient error handler always display the message as
ClientResponse has erroneous status code: 500 Internal Server Error, but when I use PostMan the API returns this JSON response with status code 500.
{
"error": {
"statusCode": 500,
"name": "Error",
"message":"Failed to add object with ID:900 as the object exists",
"stack":"some long message"
}
}
My request using WebClient is as follows
webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.retrieve()
.bodyToMono(String.class)
.doOnSuccess( args -> {
System.out.println(args.toString());
})
.doOnError( e ->{
e.printStackTrace();
System.out.println("Some Error Happend :"+e);
});
My question is, how can I get access to the JSON response when the API returns an Error with status code of 500?
If you want to retrieve the error details:
WebClient webClient = WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return Mono.just(clientResponse);
}))
.build();
with
class CustomClientException extends WebClientException {
private final HttpStatus status;
private final ErrorDetails details;
CustomClientException(HttpStatus status, ErrorDetails details) {
super(status.getReasonPhrase());
this.status = status;
this.details = details;
}
public HttpStatus getStatus() {
return status;
}
public ErrorDetails getDetails() {
return details;
}
}
and with the ErrorDetails class mapping the error body
Per-request variant:
webClient.get()
.exchange()
.map(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return clientResponse;
})
Just as #Frischling suggested, I changed my request to look as follows
return webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.exchange()
.flatMap(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError()) {
clientResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
return clientResponse.bodyToMono(String.class);
}
else
return clientResponse.bodyToMono(String.class);
});
I also noted that there's a couple of status codes from 1xx to 5xx, which is going to make my error handling easier for different cases
Look at .onErrorMap(), that gives you the exception to look at. Since you might also need the body() of the exchange() to look at, don't use retrieve, but
.exchange().flatMap((ClientResponse) response -> ....);

Get items from a single payload using a Flux

I have a method which queries a remote service. This service returns a single payload which holds many items.
How do I get those items out using a Flux and a flatMapMany?
At the moment my "fetch from service" method looks like:
public Flux<Stack> listAll() {
return this.webClient
.get()
.uri("/projects")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMapMany(response -> response.bodyToFlux(Stack.class));
}
a Stack is just a POJO which looks like:
public class Stack {
String id;
String name;
String title;
String created;
}
Nothing special here, but I think my deserializer is wrong:
protected Stack deserializeObject(JsonParser jsonParser, DeserializationContext deserializationContext, ObjectCodec objectCodec, JsonNode jsonNode) throws IOException {
log.info("JsonNode {}", jsonNode);
return Stack.builder()
.id(nullSafeValue(jsonNode.findValue("id"), String.class))
.name(nullSafeValue(jsonNode.findValue("name"), String.class))
.title(nullSafeValue(jsonNode.findValue("title"), String.class))
.created(nullSafeValue(jsonNode.findValue("created"), String.class))
.build();
}
What I've noticed happening is the first object is serialized correctly, but then it seems to get serialized again, rather than the next object in the payload.
The payload coming in follows standard JSON API spec and looks like:
{
"data":[
{
"type":"stacks",
"id":"1",
"attributes":{
"name":"name_1",
"title":"title_1",
"created":"2017-03-31 12:27:59",
"created_unix":1490916479
}
},
{
"type":"stacks",
"id":"2",
"attributes":{
"name":"name_2",
"title":"title_2",
"created":"2017-03-31 12:28:00",
"created_unix":1490916480
}
},
{
"type":"stacks",
"id":"3",
"attributes":{
"name":"name_3",
"title":"title_3",
"created":"2017-03-31 12:28:01",
"created_unix":1490916481
}
}
]
}
I've based this pattern on the spring-reactive-university
Any help as to where I've gone wrong would be awesome;
Cheers!
I think I solved it, still using a Flux.
public Flux<Stack> listAllStacks() {
return this.webClient
.get()
.uri("/naut/projects")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(String.class))
.flatMapMany(this::transformPayloadToStack);
}
Converts the incoming payload to a String where I can then parse it using a jsonapi library
private Flux<Stack> transformPayloadToStack(ResponseEntity<String> payload) {
ObjectMapper objectMapper = new ObjectMapper();
ResourceConverter resourceConverter = new ResourceConverter(objectMapper, Stack.class);
List<Stack> stackList = resourceConverter.readDocumentCollection(payload.getBody().getBytes(), Stack.class).get();
return Flux.fromIterable(stackList);
}
Which returns a Flux. Thanks to the library, I don't need to create a bunch of domains either, I can still work with my simple Stack POJO
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
#Type("stacks")
public class Stack {
#com.github.jasminb.jsonapi.annotations.Id
String id;
String name;
String title;
String created;
}
And this in turn is called from the controller
#GetMapping("/stacks")
#ResponseBody
public Flux<Stack> findAll() {
return this.stackService.listAllStacks();
}
I've not tested if this is blocking or not yet, but seems to work okay.
You json doesn't exactly match with your model class i.e. Stack. Together with Stack create another class like this
public class Data {
List<Stack> data;
// Getters and Setters....
}
Now in your webclient you can do like this
Mono<Data> listMono = webClient
.get()
.uri("/product/projects")
.exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(Data.class));
Now if you do listMono.block() you will get Data object which will have all Stack objects.

Resources