How to print responseBody with java reactor webclient before deserialization - spring

We are planing to migrate from spring Rest-Template to Reactor-webclient.
With Rest-template we have written custom logging interceptors where we were printing request and response with uniqueId, before desrialization.
Now weblient provide filters, but in filters I can't access responseBody to log it.
We have some third party APIs where they send strings in case of error and some objects in case of success. In this case I can't wait to log response after deserialization, because it will break and we will not be able to log what response we got.

You can try creating a wrapper WebClient which will first log the response and then will deserialize.
The success response will fall on doOnSuccess and the error will fall on onErrorResume.
public <T> Mono<T> get(String url, Map<String, String> headersMap, Class<T> type) {
Mono<T> responseMono = client.get().uri(url).headers((h) -> headersMap.forEach(h::set)).retrieve()
.bodyToMono(type);
return responseMono.doOnSuccess(response -> log.debug("REST GET call to {} is successfull and response is {}",
url,response).onErrorResume(err -> {
log.error("Exception occurred calling get({}): {}", url,
err.getMessage());
return Mono.error(err);
});
}

Here is some sudo code for something I use to test with:
WebClient client = WebClient.create("www.google.com");
ObjectMapper mapper = new ObjectMapper();
client.get()
.retrieve()
.bodyToMono(String.class)
.map(rawBody -> {
try {
return mapper.readValue(rawBody, Response.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("Cannot deserialize string: " + rawBody);
}
});

Related

Pact consumer test does not successfully mock the spring webclient request using the created pact

I am new to Pact Contract testing and I am trying to create a Pact consumer test to validate a method that calls an api with get request. The api request is made using Spring Webclient.
I am not able to create the webclient object by just providing the Pact mockserver eg.
WebClient webClient = WebClient.builder().baseUrl(mockServer.getUrl()).build();
I am getting the exception java.lang.IllegalStateException: No suitable default ClientHttpConnector found. The explanation I get on the internet for that , is to include reactor-netty-http and I was able to get past this issue when i included that in the POM. But I don't think that is the right solution here because I need the mockserver to respond to the webclient request and it is not. Has anyone dealt with this issue before or am I doing this wrong?
Here is the code snippet:
public RequestResponsePact pactMethod(PactDslWithProvider builder) {
Map<String, String> headers = new HashMap<>();
headers.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return builder.given("Consumer request")
.uponReceiving(" getResource call")
.path("/path")
.method("GET")
.willRespondWith()
.status(200)
.headers(headers)
.body(RESPONSE_JSON).toPact();
}
#Test
#PactTestFor(pactMethod = "pactMethod", port = "9999")
public void consumerTest(MockServer mockServer) {
WebClient webClient = WebClient.builder().baseUrl(mockServer.getUrl()).build();
ConsumerServiceClient consumerServiceClient = new ConsumerServiceClient(webClient);
Mono<Data> data = consumerServiceClient.getData();
StepVerifier.create(data)
.assertNext(resp -> {
try {
Value value = resp.getValue();
Assertions.assertFalse( value.isEmpty());
} catch (Exception e) {
log.error("Unable to convert response to Value", e);
Assertions.fail();
}
}).expectComplete()
.verify();
}
The webclient call:
webClient.get()
.uri("/path")
.retrieve()
.onStatus(status -> status == HttpStatus.NOT_FOUND,
res -> Mono.error(new RunTimeException()))
.bodyToMono(clazz);

Spring WebClient - Stop retrying if an exception is thrown in the doOnError

I have the following code to make a request that is going to be retried a max number of times. This request needs an authorization header and I'm caching this information to prevent this method to call the method to retrieve this information every time.
What I'm trying to do is:
When calling myMethod I first retrieve the login information for the service I'm calling, in most cases that will come from the cache when calling the getAuthorizationHeaderValue method.
In the web client, if the response to send this request returns a 4xx response I need to login again to the service I'm calling, before retrying the request. For that, I'm calling the tryToLoginAgain method to set the value for the header again.
After doing that the retry of the request should work now that the header has been set.
If by any chance the call to login again fails I need to stop retrying as there no use on retrying the request.
public <T> T myMethod(...) {
...
try {
AtomicReference<String> headerValue = new AtomicReference<>(loginService.getAuthorizationHeaderValue());
Mono<T> monoResult = webclient.get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, headerValue.get())
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> throwHttpClientLoginException())
.bodyToMono(type)
.doOnError(HttpClientLoginException.class, e -> tryToLoginAgain(headerValue))
.retryWhen(Retry.backoff(MAX_NUMBER_RETRIES, Duration.ofSeconds(5)));
result = monoResult.block();
} catch(Exception e) {
throw new HttpClientException("There was an error while sending the request");
}
return result;
}
...
private Mono<Throwable> throwHttpClientLoginException() {
return Mono.error(new HttpClientLoginException("Existing Authorization failed"));
}
private void tryToLoginAgain(AtomicReference<String> headerValue) {
loginService.removeAccessTokenFromCache();
headerValue.set(loginService.getAuthorizationHeaderValue());
}
I have some unit tests and the happy path works fine (unauthorized the first time, try to login again and send the request again) but the scenario where the login doesn't work at all is not working.
I thought that if the tryToLoginAgain method throws an Exception that would be caught by the catch I have in myMethod but it never reaches there, it just retries the request again. Is there any way to do what I want?
So at the end I found a way of doing what I wanted and now the code looks like this:
public <T> T myMethod() {
try {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getAuthorizationHeaderValue());
final RetryBackoffSpec retrySpec = Retry.backoff(MAX_NUMBER_RETRIES, Duration.ofSeconds(5))
.doBeforeRetry(retrySignal -> {
//When retrying, if this was a login error, try to login again
if (retrySignal.failure() instanceof HttpClientLoginException) {
tryToLoginAgain(headers);
}
});
Mono<T> monoResult = Mono.defer(() ->
getRequestFromMethod(httpMethod, uri, body, headers)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> throwHttpClientLoginException())
.bodyToMono(type)
)
.retryWhen(retrySpec);
result = monoResult.block();
} catch (Exception e) {
String requestUri = uri != null ?
uri.toString() :
endpoint;
log.error("There was an error while sending the request [{}] [{}]", httpMethod.name(), requestUri);
throw new HttpClientException("There was an error while sending the request [" + httpMethod.name() +
"] [" + requestUri + "]");
}
return result;
}
private void tryToLoginAgain(HttpHeaders httpHeaders) {
//If there was an 4xx error, let's evict the cache to remove the existing access_token (if it exists)
loginService.removeAccessTokenFromCache();
//And let's try to login again
httpHeaders.setBearerAuth(getAuthorizationHeaderValue());
}
private Mono<Throwable> throwHttpClientLoginException() {
return Mono.error(new HttpClientLoginException("Existing Authorization failed"));
}
private WebClient.RequestHeadersSpec getRequestFromMethod(HttpMethod httpMethod, URI uri, Object body, HttpHeaders headers) {
switch (httpMethod) {
case GET:
return webClient.get()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON);
case POST:
return body == null ?
webClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON) :
webClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body);
case PUT:
return body == null ?
webClient.put()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON) :
webClient.put()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body);
case DELETE:
return webClient.delete()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON);
default:
log.error("Method [{}] is not supported", httpMethod.name());
throw new HttpClientException("Method [" + httpMethod.name() + "] is not supported");
}
}
private String getAuthorizationHeaderValue() {
return loginService.retrieveAccessToken();
}
By using Mono.defer() I can retry on that Mono and make sure I change the headers I'll use with the WebClient. The retry spec will check if the exception was of the HttpClientLoginException type, thrown when the request gets a 4xx status code and in that case it will try to login again and set the header for the next retry. If the status code was different it will retry again using the same authorization.
Also, if there's an error when we try to login again, that will be caught by the catch and it won't retry anymore.

How to call a microservice to fetch data in spring webflux

I want to call a microservice from another service using webclient in spring flux. But, I am not able to write the code properly. Can you please suggest how to call another service. Please find my code as below.
I need to call the below service
public Mono<ServerResponse> load(ServerRequest res){
String c1name = res.pathVariable("cust");
String c2name = res.queryParam("cl").orElse("");
String oname = res.queryParam("ol").orElse("");
return res.body()
}
public Mono<ResponseEntity<Void>> ftpFileSend(MultipartFile fileData, String cust, MultiValueMap<String,String) qpar {
MultiValueMap<String,String> qpar=new LinkedMultiValueMap<String,String>();
qpar.add("name","spring");
MultiValueMap<String,Object> body=new LinkedMultiValueMap<String,Object>();
String url="http://localhost:8088/"+ cust+"/load";
try {
body.add("file", fileData.getBytes());
} catch (IOException e) {
return Mono.error(e); // <-- note how to create an error signal
}
return webClient
.post()
.uri(uriBuilder -> uriBuilder.path(url).queryParams(qpar).build() )
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(body))
.retrieve()
.toBodilessEntity();
}
Hmm it would be great if you have provided some error logs or so. Anyway if you want to create a multipart body there is a builder, MultipartBodyBuilder (in org.springframework.http.client.MultipartBodyBuilder).
Example usage is as follows,
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("file", new MultipartFileResource(fileData));
MultiValueMap<String, HttpEntity<?>> multipartBody = builder.build();
Then use this multipartBody in webClient call.
return webClient
...
.body(BodyInserters.fromMultipartData(multipartBody))
.retrieve()
.toBodilessEntity();

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,

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();

Resources