How to use Spring WebClient to make multiple calls simultaneously and get response separately? - spring

I have to execute multiple API calls simultaneously which are independent of each other:
Mono<Response1> response1= this.webClient
.post()
.uri(requestURI1)
.body(Flux.just(request.getRequestBody()), ParameterizedTypeReference<T>)
.exchangeToMono(response -> {
return response.statusCode().equals(HttpStatus.OK)
? response.bodyToMono(ParameterizedTypeReference<T>)
: response.createException().flatMap(Mono::error);
});
Mono<Response2> response2= this.webClient
.post()
.uri(requestURI2)
.body(Flux.just(request.getRequestBody()), ParameterizedTypeReference<T>)
.exchangeToMono(response -> {
return response.statusCode().equals(HttpStatus.OK)
? response.bodyToMono(ParameterizedTypeReference<T>)
: response.createException().flatMap(Mono::error);
});
Mono<Response3> response3= this.webClient
.post()
.uri(requestURI3)
.body(Flux.just(request.getRequestBody()), ParameterizedTypeReference<T>)
.exchangeToMono(response -> {
return response.statusCode().equals(HttpStatus.OK)
? response.bodyToMono(ParameterizedTypeReference<T>)
: response.createException().flatMap(Mono::error);
});
How can I get the response of above api calls in separate Objects and at the same time, they should be executed in parallel? Also, after executing these above calls and putting the data in separate objects, say, Response1, Response2, Response3, I want to execute another API call which consumes these responses Response1, Response2, Response3.
I have tried to use Flux.merge but this will merge the responses in single object which is not correct. Also read about Mono.zip, but these all are used to combine the responses which I don't want.
EDIT:
Mono.zip works perfectly.
I have a follow up question which I have mentioned in comments but posting it here also.
So I have implemented like this:
Mono.zip(rs1, rs2, rs3).flatMap(tuples -> {
//do something with responses
return Mono.just(transformedData)
}).flatMap(transformedData -> {
//here another webclient.post call which consumes transformedData and return actualData in form of Mono<actualData>
Mono<actualData> data= callAPI;
return data;
});
Now this response is propagated to rest layer in form of Mono<actualData> and I am getting this in response: {
"scanAvailable": true
}

To combine publishers in parallel you can use Mono.zip that will return TupleX<...> and will be resolved when all publishers are resolved.
Mono<Tuple3<Response1, Response2, Response3>> res = Mono.zip(response1, response2, response3)
.map(tuples -> {
//do something with responses
return transformedData;
})
.flatMap(transformedData -> {
//here another webclient.post call which consumes transformedData and return actualData in form of Mono<actualData>
return callAPI(transformedData);
});
Depending on the error handling logic you could consider zipDelayError
As I mentioned in comment one of the key things with reactive API is to differentiate sync and async operations. In case of sync transformation, chain it with .map and use .flatMap or similar operator in case you want to chain another async method.

Related

Why is the log statement in Spring WebClient subscribe() block not invoked?

I am playing around with the spring webclient to send emails via an api. The mails get send, however I am trying to do this without blocking and I want to log the successful response (or error). As I understood it so far, I could do this in the subscribe block. However, I do not manage to get the logging (or anything else) within the subscribe block to work.
MailClient.kt
fun sendMail(mail: Mail): Mono<String> {
return getWebClient()
.post()
.uri("uri")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(..)
.retrieve()
.bodyToMono(String::class.java)
//.timeout(Duration.ofSeconds(10))
}
Service function which uses MailClient:
logger.info("Sending mail due to failure with id = ${failure.id}, error: $errorMessage")
mailClient.sendMail(
Mail(..)
)
.subscribe(
{ response -> logger.info("success: $response") },
{ error -> logger.error("error: $error") })

Communication between Spring boot micro services fail When trying to call from .map

I want to write a Rest API using reactive and achieve the below result. I've 2 microservices for this.
Service 1: Receives a post Object and retrieves some data from its database. The process something then communicate to the second Micro Service. Then return the Service 2 response as a Mono to the client.
Service 2: Receives the Data passed from Service 1 and save it to database and returns the Object with the generated primary key.
Here is my code look like.
#PostMapping("/submitData")
public Mono<Object> submitScore(#RequestBody SomeObject someObject){
//Some Operations
return contestGiftInfoRepository.findAllDataById(id)
.filter(c -> {
if(condition) {
return true;
} else {
return false;
}
}).reduce((item1,item2) -> {
if(condition) {
return item1;
}else {
return item2;
}
})
.defaultIfEmpty(emptyObj)
.map(resultObj->{
//DoSome OPeration
WebClient client = WebClient.create("my service Url 2");
Mono<Obj> response = client.post().uri("/ExecutionPath")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(Mono.just(resultObj), ResultObj.class)
.retrieve()
.bodyToMono(ResponseObj.class);
return response;
});
}
Now When I call the web service 1 everything works fine. When I call the second web service individually that works as well. However, when I call the second web service from the first service it returns a result reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError: 500 Internal Server Error from POST http://localhost:8085/path Caused by: org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError: 500 Internal Server Error from POST http://localhost:8085/path at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:218) ~[spring-webflux-5.3.16.jar:5.3.16] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s):
I don't understand the actual issue from the error message. What I'm missing here?

Sending appropriate error responses on web actions

I have some web-enabled actions that are exposed through API Connect in IBM Cloud Serverless Functions.
Some of my actions use request-promises to call external REST services and I need to be able to catch an error and respond with an appropriate status-code to the caller.
Since the actions are web-enabled, the documentation indicates that I can use an annotated JSON to set the headers, status-code and body of the response. But it seems that, seems the API expects to always get a Content-Type=application/json, the response processor is failing to understand my annotations in the case of an error.
I tried the following without success:
let rp = require('request-promise');
function main(params){
//setup options
return rp(options).then(
res => {
return res;
}
).catch(
err => {
return { error: { statusCode:err.statusCode } }
}
);
}
Another variation:
let rp = require('request-promise');
function main(params){
//setup options
return rp(options).then(
res => {
return res;
}
).catch(
err => {
return { statusCode:err.statusCode }
}
);
}
The problem is that the status-code I always get is 200... I also tried to change the runtime to node8.0 without success.
Thanks!
I found the answer myself :)
In order to get the status-code and headers, one must set the field Response Content Type to `Use "Content-Type" header from action", while setting up the mapping between the API call and the action....

Spring WebClient async callback not called when http server response 404

A problem I encountered is as titled, and the code is below:
Mono<Account> accountMono = client.get()
.uri("accounturl")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Account.class);
} else {
return Mono.empty();
}
});
accountMono.subscribe(result -> callback(result));
```
Server response 404. I try to create an empty Account, but the callback() is not called. It looks like the empty Mono is not emitted.
Server response 404, I try to create a empty Account
You're not creating an empty Account. You're returning an empty Mono, i.e. a Mono that will never emit anything.
If you want to return a Mono which emits an empty Account, then you need
return Mono.just(new Account());

Axios Reponse Interceptor : unable to handle an expired refresh_token (401)

I have the following interceptor on my axios reponse :
window.axios.interceptors.response.use(
response => {
return response;
},
error => {
let errorResponse = error.response;
if (errorResponse.status === 401 && errorResponse.config && !errorResponse.config.__isRetryRequest) {
return this._getAuthToken()
.then(response => {
this.setToken(response.data.access_token, response.data.refresh_token);
errorResponse.config.__isRetryRequest = true;
errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
return window.axios(errorResponse.config);
}).catch(error => {
return Promise.reject(error);
});
}
return Promise.reject(error);
}
);
The _getAuthToken method is :
_getAuthToken() {
if (!this.authTokenRequest) {
this.authTokenRequest = window.axios.post('/api/refresh_token', {
'refresh_token': localStorage.getItem('refresh_token')
});
this.authTokenRequest.then(response => {
this.authTokenRequest = null;
}).catch(error => {
this.authTokenRequest = null;
});
}
return this.authTokenRequest;
}
The code is heavily inspired by https://github.com/axios/axios/issues/266#issuecomment-335420598.
Summary : when the user makes a call to the API and if his access_token has expired (a 401 code is returned by the API) the app calls the /api/refresh_token endpoint to get a new access_token. If the refresh_token is still valid when making this call, everything works fine : I get a new access_token and a new refresh_token and the initial API call requested by the user is made again and returned correctly.
The problem occurs when the refresh_token has also expired.
In that case, the call to /api/refresh_token returns a 401 and nothing happens. I tried several things but I'm unable to detect that in order to redirect the user to the login page of the app.
I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...
I'm a newbie with Promises so I may be missing something !
Thanks for any help !
EDIT :
I may have found a way much simpler to handle this : use axios.interceptors.response.eject() to disable the interceptor when I call the /api/refresh_token endpoint, and re-enable it after.
The code :
createAxiosResponseInterceptor() {
this.axiosResponseInterceptor = window.axios.interceptors.response.use(
response => {
return response;
},
error => {
let errorResponse = error.response;
if (errorResponse.status === 401) {
window.axios.interceptors.response.eject(this.axiosResponseInterceptor);
return window.axios.post('/api/refresh_token', {
'refresh_token': this._getToken('refresh_token')
}).then(response => {
this.setToken(response.data.access_token, response.data.refresh_token);
errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
this.createAxiosResponseInterceptor();
return window.axios(errorResponse.config);
}).catch(error => {
this.destroyToken();
this.createAxiosResponseInterceptor();
this.router.push('/login');
return Promise.reject(error);
});
}
return Promise.reject(error);
}
);
},
Does it looks good or bad ? Any advice or comment appreciated.
Your last solution looks not bad. I would come up with the similar implementation as you if I were in the same situation.
I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...
That's because this.authTokenRequest in the code was just assigned the Promise created from window.axios.post. Promise is an object handling kind of lazy evaluation, so the process you implement in then is not executed until the Promise was resolved.
JavaScript provides us with Promise object as kind of asynchronous event handlers which enables us to implement process as then chain which is going to be executed in respond with the result of asynchronous result. HTTP requests are always inpredictable, because HTTP request sometimes consumes much more time we expect, and also sometimes not. Promise is always used when we use HTTP request in order to handle the asynchronous response of it with event handlers.
In ES2015 syntax, you can implement functions with async/await syntax to hanle Promise objects as it looks synchronous.

Resources