Spring webClient generate NullPointException after going through the filter - spring

I want to do exception handling. When StatusCode 200, but not the desired body.
To make the processing global I am using filter.
below is the code.
public WebClient webClient() {
return WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(response -> {
// statusCode 400 || 500
if (response.statusCode().isError()) {
return response.bodyToMono(String.class)
.flatMap(body ->
Mono.error(new ExternalClientException(body))
)
);
}
// statusCode 200
return response.bodyToMono(String.class)
.flatMap(body -> {
if (YanoljaConstants.EMPTY_BODY.contains(body)) {
return Mono.error(new ExternalClientException(body))
.cast(ClientResponse.class);
}
return Mono.just(response); // normal
});
}))
.build();
}
And the actual client code is shown below.
public Foo getPlace(int no) {
return Objects.requireNonNull(contentsApiV2Client.get()
.uri(uriBuilder -> uriBuilder.path("/path").build(no))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Foo>() {
})
.block());
}
When using this, NullPointerException occurs when responding normally after going through the filter(return Mono#just(response)).
I want clear this issue.
For reference, the below code works normally.
public WebClient webClient() {
return WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(response -> {
// statusCode 400 || 500
if (response.statusCode().isError()) {
return response.bodyToMono(String.class)
.flatMap(body ->
Mono.error(new ExternalClientException(body))
)
);
}
return Mono.just(response); // normal
}))
.build();
}
Thank you. waiting for your answer!
Solve!
public WebClient webClient() {
return WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(response -> {
// statusCode 400 || 500
if (response.statusCode().isError()) {
return response.bodyToMono(String.class)
.flatMap(body ->
Mono.error(new ExternalClientException(body))
)
);
}
// statusCode 200
return response.bodyToMono(String.class)
.flatMap(body -> {
if (YanoljaConstants.EMPTY_BODY.contains(body)) {
return Mono.error(new ExternalClientException(body))
.cast(ClientResponse.class);
}
return Mono.just(ClientResponse.create(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.body(body)
.build()); // normal
});
}))
.build();
}
I realized that running the monoTobody method consumes it and the value disappears. So I solved it by creating a new ClientResponse.
Thank you.

Related

How to mock callbacks with mockito correctly?

I have a method like this:
override fun functionToBeMocked(
param: Param,
onSuccess: (param: Param) -> Unit,
onError: (param: Param, errorMessage: String) -> Unit
) {
val response = factory.request(param)
.exchange()
.block()
if (response?.statusCode()?.is2xxSuccessful == true {
onSuccess(param)
} else if (response?.statusCode()?.isError == true) {
val status = response.rawStatusCode()
val body = response.bodyToMono(String::class.java).block()
val errorMessage = "$status : $body"
onError(param, errorMessage)
} else {
return
}
}
I want to test the service which calls this method with the given onSuccess and onError functions. How can I mock functionToBeMocked() to just return onSuccess(param) or onError(param)?
Current test:
#Test
fun test() {
val failure = ParamDoc(param.id, param)
whenever(repo.findAll()).thenReturn(listOf(failure))
// This method call should just execute onSuccess() or onError depending on the testcase
// whenever(mockedService.functionToBeMocked).thenAnswer.. (?)
underTest.functionToBeTested()
// verify(..)
}
Update: request function in factory:
fun request(param: Param): WebClient.RequestHeadersSpec<*> {
val client = WebClient
.builder()
.defaultHeader("API-KEY", config.apiKey)
.baseUrl(config.baseUrl)
.build()
return client
.post()
.uri("/service/test")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(Dto.fromParam(param))
}

Webclient retryWhen and onErrorResume mutually exclusive?

I am trying to implement a retry on specific exception but I could not make it work using the following:
return client
.sendWebhook(request, url)
.exchangeToMono(
response -> {
final HttpStatus status = response.statusCode();
return response
.bodyToMono(String.class)
.defaultIfEmpty(StringUtils.EMPTY)
.map(
body -> {
if (status.is2xxSuccessful()) {
log.info("HTTP_SUCCESS[{}][{}] body[{}]", functionName, company, body);
return ResponseEntity.ok().body(body);
} else {
log.warn(
format(
"HTTP_ERROR[%s][%s] status[%s] body[%s]",
functionName, company, status, body));
return status.is4xxClientError()
? ResponseEntity.badRequest().body(body)
: ResponseEntity.internalServerError().body(body);
}
});
})
.retryWhen(
Retry.backoff(1, Duration.ofSeconds(1))
.filter(
err -> {
if (err instanceof PrematureCloseException) {
log.warn("PrematureCloseException detected retrying.");
return true;
}
return false;
}))
.onErrorResume(
ex -> {
log.warn(
format(
"HTTP_ERROR[%s][%s] errorInternal[%s]",
functionName, company, ex.getMessage()));
return Mono.just(ResponseEntity.internalServerError().body(ex.getMessage()));
});
It seems that the retry is never getting called on PrematureCloseException.
Resolved, it was not working because of rootCause
Retry.backoff(3, Duration.ofMillis(500))
.filter(
ex -> {
if (ExceptionUtils.getRootCause(ex) instanceof PrematureCloseException) {
log.info(
"HTTP_RETRY[{}][{}] PrematureClose detected retrying", functionName, company);
return true;
}
return false;
});

Am I not allowed to return response in exchangeToMono with Mono.just?

I want to return a header and body as a form of data responded from WebClient.
here is a code that I have tried
var res = webclient.post()
.uri(URI)
.accept(MediaType.APPLICATION_JSON)
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setBearerAuth(token);
})
.bodyValue(body)
.exchangeToMono(response -> {
if (response.statusCode().value() >= 204) {
return response.bodyToMono(String.class)
.flatMap(message -> Mono.error(new MyException(message, response.statusCode())));
}
return Mono.just(response);
})
.switchIfEmpty(Mono.error(
new SimulatorException(
String.format("Null has returned", country),
HttpStatus.INTERNAL_SERVER_ERROR
)
))
.block();
res.headers().asHttpHeaders().toSingleValueMap() // No problem
res.bodyToMono(ResBodyClass.class).single().block() // NullPointerException
So I thought empty body might has returned, but
var resBody = webclient.post()
.uri(URI)
.accept(MediaType.APPLICATION_JSON)
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setBearerAuth(token);
})
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.single().block();
System.out.println(resBody); // It is not null...
it worked fine it this way.
It is really hard to understand why response has no body at all while in exchangeToMono
What am I missing right now?

How to return a object from Spring Flux flatmap operation

I am looking to return a Mono.just(file.getAbsolutePath()) after I have saved the file. The following is my code:
public Mono<String> save(Mono<FilePart> filePartMono) {
Mono<String> monoString = filePartMono.flatMap(filePart -> {
File file = new File(filePart.filename());
if (file.exists()) {
file.delete();
LOG.info("existing file deleted: {}", file.getAbsolutePath());
}
Mono<Void> mono = filePart.transferTo(file);
LOG.info("file saved: {}", file.getAbsolutePath());
return Mono.just(file.getAbsolutePath());
}).thenReturn("hello");
return monoString;
Right now I am returning a "hello". Is there a way I can return the file.getAbsolutePath() instead of the string out of my save() method?
I think it can be done like this:
public Mono<String> save(Mono<FilePart> filePartMono) {
return filePartMono.flatMap(filePart -> {
File file = new File(filePart.filename());
if (file.exists()) {
file.delete();
log.info("existing file deleted: {}", file.getAbsolutePath());
}
return filePart.transferTo(file)
.doOnNext(v -> {
log.info("file saved: {}", file.getAbsolutePath());
}).thenReturn(file.getAbsolutePath());
});
}

Adding new header when retrying with Spring WebClient

webclientbuilder.baseUrl(url)
.defaultHeaders(headers -> headers.addAll(requestHeader))
.build()
.post()
.uri("/uri")
.bodyValue(data)
.exchange()
.flatMap(response -> {
if(response.statusCode() == HttpStatus.UNAUTHORIZED){
//retry with updated token in header
}
})
//return bodyToMono of specific object when retry is done or if
//response status is 2xx
Any advice on how to deal with this is appreciated! As the comments say, I need to add the new token to the header before I retry the post request if there is a statusCode of UNAUTHORIZED, and if statusCode of 2xx, then return bodyToMono.
You can solve this by adding a filter to your webclient:
public ExchangeFilterFunction retryOn401() {
return (request, next) -> next.exchange(request)
.flatMap((Function<ClientResponse, Mono<ClientResponse>>) clientResponse -> {
if (clientResponse.statusCode() == HttpStatus.UNAUTHORIZED) {
return authClient.getUpdatedToken() //here you get a Mono with a new & valid token
.map(token -> ClientRequest
.from(request)
.headers(headers -> headers.replace("Authorization", Collections.singletonList("Bearer " + token)))
.build())
.flatMap(next::exchange);
} else {
return Mono.just(clientResponse);
}
});
}
Hence, when the webclient retries the unauthorized request, it can obtain a new token and set it on the header before performing the retry.
Make sure to add it to the webclient:
webclientbuilder.baseUrl(url)
.defaultHeaders(headers -> headers.addAll(requestHeader))
.filter(retryOn401())
.build();

Resources