Spring WebClient : Implement Fallback method - spring

I want to call my fall-back API when my actual API is taking more than 1 second
#GetMapping("/{id}")
public String getDetailsById(#PathVariable Long id) {
var url = getAPIUrl(id);
var response = webClient.get()
.uri(url)
.retrieve()
.onStatus(HttpStatus::isError,this::myFallBackMethod)
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(1))
.block();
return response;
}
private Mono<? extends Throwable> myFallBackMethod(ClientResponse clientResponse) {
return Mono.just("");
}
I get two compile exceptions
Incompatible types
and
cannot resolve methoe myFallBackMethod
How to handle fall backs and return the String ?

I was able to do that my calling the function onErrorResume
#GetMapping("/{id}")
public String getDetailsById(#PathVariable Long id) {
var url = getAPIUrl(id);
var response = webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(1))
.onErrorResume(throwable -> myFallBackMethod(id,throwable))
.block();
return response;
}
private Mono<? extends String> myFallBackMethod(Long id, Throwable throwable) {
return Mono.just("test sample");
}

Related

Spring boot webflux annotated controller aspect trying to log the request Mono

So I have this around aspect applied to my controller method to log the request and response which works fine if I do not wrap the request in a Mono-
#PostMapping(
value = TRANSACTION_INSIGHTS_RETRIEVALS_PATH,
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
#AuditLogEvent(logEventType = LogEventTypeEnum.TRA_REQUEST_RESPONSE)
public Mono<ResponseEntity<TransactionInsights>> retrieveTransactionInsights(#Valid #RequestBody TransactionInsightsData transactionInsightsData) {
return Mono.just(transactionInsightsData)
.flatMap(request -> {
Optional<String> brand = retrieveBrand(request);
return transactionInsightsService.retrieveTransactionInsights(request, brand);
})
.map(ResponseEntity::ok);
}
My aspect -
#Slf4j
#Aspect
#Order(3)
#Component
#RequiredArgsConstructor
public class AuditLogEventAspect {
private final AuditEventManager auditEventManager;
#Around("#annotation(auditLogEvent) && args(request)")
public Object logAround(ProceedingJoinPoint joinPoint, AuditLogEvent auditLogEvent, Object request) throws Throwable {
Mono result = (Mono) joinPoint.proceed();
return Mono.deferContextual(ctx -> result
.doOnSuccess(o -> {
logSuccessExit(auditLogEvent, ctx.<Map<String, String>>get(CONTEXT_MAP), request, o)
.subscribe(auditEvent -> {
log.info("auditEvent {}", auditEvent);
});
})
.doOnError(o -> {
logErrorExit(auditLogEvent, ctx.<Map<String, String>>get(CONTEXT_MAP), request, (Exception) o)
.subscribe(auditEvent -> {
log.info("auditEvent {}", auditEvent);
});
})
.map(o -> result)
);
}
private Mono<AuditEvent> logErrorExit(AuditLogEvent auditLogEvent, Map<String, String> contextMap, Object request, Exception error) {
log.info("TRA request: {}", request);
log.info("TRA error response: {}", error.getMessage());
AuditEvent initialAuditEvent = auditEventManager.createAuditLogEvent(auditLogEvent, contextMap, request);
return auditEventManager.saveExceptionEvent(initialAuditEvent, error);
}
private Mono<AuditEvent> logSuccessExit(AuditLogEvent auditLogEvent, Map<String, String> contextMap, Object request, Object response) {
log.info("TRA request: {}", request);
log.info("TRA response: {}", response);
AuditEvent initialAuditEvent = auditEventManager.createAuditLogEvent(auditLogEvent, contextMap, request);
return auditEventManager.saveSuccessEvent(initialAuditEvent, response);
}
}
But ideally I'd like to wrap my request in a Mono like so -
#PostMapping(
value = TRANSACTION_INSIGHTS_RETRIEVALS_PATH,
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
#AuditLogEvent(logEventType = LogEventTypeEnum.TRA_REQUEST_RESPONSE)
public Mono<ResponseEntity<TransactionInsights>> retrieveTransactionInsights(#Valid #RequestBody Mono<TransactionInsightsData> transactionInsightsData) {
return transactionInsightsData
.flatMap(request -> {
Optional<String> brand = retrieveBrand(request);
return transactionInsightsService.retrieveTransactionInsights(request, brand);
})
.map(ResponseEntity::ok);
}
How can I change my aspect to work with arg Mono? I am new to reactive and webflux and learning so what am I missing?

How do I make a post / get request to a endpoint with a requestHeader?

Method in question
#GetMapping("/all")
public Mono<ResponseEntity<String>> getSomeData(#RequestHeader String someId) {
...some code
}
Tried to call the consume the endpoint with this method:
#Autowired
WebClient.Builder webClient;
String someString = webClient.
.get()
.uri(someUrl)
.header("someId", "someString")
.retrieve()
.bodyToMono(String.class)
.block();
I got a status 415 with Unsupported media type with "Content type '' not supported"
How do I use webClientBuilder to set my id header?
You just need to set the correct content-type. If your controller expects it to be "plain/text" you might have to set that explicitly within your requesting client. 415 does indicate a miss match.
As mentioned by #Alex you are autowiring builder instead look for the concrete implementation of WebClient. Please check my WebClient config bean. But that is not the actual issue.
When you are sending body with webClient you have to use
.body(...)
so for sending plain text body where controller is expecting plain body you need something like below:
.body(BodyInserters.fromProducer(Mono.just("random body"), String.class))
and when controller is expecing an object is request you need to use something like this
.body(BodyInserters.fromProducer(Mono.just(new Greet("Hello there this is the body of post request")), Greet.class))
Greet.java
public static class Greet {
String name;
public Greet() {
}
public Greet(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Configuration of WebCLient
#Configuration
class WebClientConfig {
#Bean
WebClient webClient() {
return WebClient.builder().baseUrl("http://localhost:8080/").build();
}
}
#RequestMapping("/sample")
#RestController
static class SampleComntroller {
private final WebClient webClient;
#Autowired
SampleComntroller(WebClient webClient) {
this.webClient = webClient;
}
#GetMapping(value = "/main-get")//, consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<String> helloGet(#RequestHeader(name = "someId") String someId) {
return Mono.just("Hello, Spring!, get, response with header is=>" + someId);
}
#PostMapping(value = "/main-post-plain-string", consumes = MediaType.TEXT_PLAIN_VALUE)
public Mono<String> helloPost(#RequestHeader(name = "someId") String someId, #RequestBody String body) {
return Mono.just("Hello, Spring!, post, response with header is=>" + someId + " and random body " + UUID.randomUUID().toString());
}
#PostMapping(value = "/main-post-object", consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<String> helloPostObject(#RequestHeader(name = "someId") String someId, #RequestBody Greet greet) {
return Mono.just("Hello, Spring!, post, response with header is=>" + someId + " " + greet.getName() + " " + UUID.randomUUID().toString());
}
#GetMapping("/delegate-get")
public String delegateGet() {
return webClient
.get()
.uri("/sample/main-get")
.header("someId", "178A-0E88-get")
.retrieve().bodyToMono(String.class).block();
}
#PostMapping("/delegate-post")
public String delegatePost() {
return webClient
.post()
.uri("/sample/main-post-plain-string")
.body(BodyInserters.fromProducer(Mono.just("random body"), String.class))
.header("someId", "178A-0E88-post")
.retrieve()
.bodyToMono(String.class).block();
}
#PostMapping("/delegate-post-object")
public String delegatePostObject() {
return webClient
.post()
.uri("/sample/main-post-object")
.body(BodyInserters.fromProducer(Mono.just(new Greet("Hello there this is the body of post request")), Greet.class))
.header("someId", "178A-0E88-post")
.retrieve()
.bodyToMono(String.class).block();
}
}

How to receive a Map<String, Integer> from an endpoint using Spring WebClient get?

How can I receive a Map<String, Integer> from an endpoint web service using WebClient in Spring Boot? Here is my try: (it gives syntax error: Incompatible equality constraint: Map<String, Integer> and Map). How can I fix it?
public Flux<Map<String, Integer>> findAll(String param1, String param2) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/url")
.queryParam("param1", param1)
.queryParam("param2", param2)
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(Map.class);
}
For generic types, like the Map, you should use ParameterizedTypeReference instead of a class in the call to the bodyToFlux method:
public Flux<Map<String, Integer>> findAll(String param1, String param2) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/url")
.queryParam("param1", param1)
.queryParam("param2", param2)
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(new ParameterizedTypeReference<>() {});
}
In practice, probably you would like to define a constant for the type reference:
private static final ParameterizedTypeReference<Map<String, Integer>> MAP_TYPE_REF = new ParameterizedTypeReference<>() {};
public Flux<Map<String, Integer>> findAll(String param1, String param2) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/url")
.queryParam("param1", param1)
.queryParam("param2", param2)
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(MAP_TYPE_REF);
}

Spring WebFlux : WebClient + Fallback on error

I want to set up a fallback when my program works if the first service is unavailable.
On the second service, I use WebClient which accesses the first service and receives data from it.
I made two options but they do not work for me.
If both services are alive, then everything works well.
If the first service is unavailable, then when I try to send a request via WebClient, nothing happens, I see a blank screen in the browser.
1) The first option:
#Service
public class WebClientService {
private static final String API_MIME_TYPE = "application/json";
private static final String API_BASE_URL = "http://localhost:8081";
private static final String USER_AGENT = "User Service";
private static final Logger logger = LoggerFactory.getLogger(WebClientService.class);
private WebClient webClient;
public WebClientService() {
this.webClient = WebClient.builder()
.baseUrl(API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.filter(WebClientService.errorHandlingFilter())
.build();
}
public Flux<Bucket> getDataByWebClient() {
return webClient
.get()
.uri("/getAll")
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(Bucket.class));
}
public static ExchangeFilterFunction errorHandlingFilter() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if(clientResponse.statusCode()!=null && (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError()) ) {
return clientResponse.bodyToMono(String.class)
.flatMap(errorBody -> {
return Mono.error(new MyCustomServerException());
});
}else {
return Mono.just(clientResponse);
}
});
}
}
Class MyCustomServerException
public class MyCustomServerException extends Throwable {
public String getAllEmployeesList() {
return "Server error";
}
public MyCustomServerException() {
getAllEmployeesList();
}
}
2) The second option:
#Service
public class WebClientService {
private static final String API_MIME_TYPE = "application/json";
private static final String API_BASE_URL = "http://localhost:8081";
private static final String USER_AGENT = "User Service";
private static final Logger logger = LoggerFactory.getLogger(WebClientService.class);
private WebClient webClient;
public WebClientService() {
this.webClient = WebClient.builder()
.baseUrl(API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.build();
}
public Flux<Bucket> getDataByWebClient() {
return webClient
.get()
.uri("/stream/buckets/delay")
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(Bucket.class));
}
public Flux<Bucket> getDataByWebClient() {
return webClient
.get()
.uri("/getAll")
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
System.out.println("4xx eror");
return Mono.error(new RuntimeException("4xx"));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
System.out.println("5xx eror");
return Mono.error(new RuntimeException("5xx"));
})
.onStatus(HttpStatus::isError, clientResponse -> {
System.out.println("eror");
return Mono.error(new MyCustomServerException());
})
.bodyToFlux(Bucket.class);
}
}
Why is this not working? Can anyone tell me?
I want the browser to display the message "Server error" from my class with an error.
Thanks!

Springboot 2.1.x Webflux functional endpoints - How to perform input validation?

I am trying to integrate validation in this code, but it fails without any error (just a blank page being returned):
#Component
#Slf4j
public class EthereumAccountController implements ClarityControllerMono {
private final Pipeline queryPipeline;
private final Pipeline commandPipeline;
private final Web3jService web3;
private final RequestHandler requestHandler;
public EthereumAccountController(#Qualifier("queryPipelinr") Pipeline queryPipeline, #Qualifier("commandPipelinr") Pipeline commandPipeline, Web3jService web3, RequestHandler requestHandler) {
this.queryPipeline = queryPipeline;
this.commandPipeline = commandPipeline;
this.web3 = web3;
this.requestHandler = requestHandler;
}
public Mono<ServerResponse> createAccount(ServerRequest serverRequest) {
return requestHandler.requireValidBody(body -> getJsonErrsResp("testtttt"), serverRequest, AccountRequestDTO.class);
}
}
public interface ClarityControllerMono {
default Mono<ServerResponse> getJsonSuccessResp (Object object) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("status", "success");
result.put("data", object);
return ok()
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromPublisher(Mono.just(toJSON(result)), String.class));
}
default Mono<ServerResponse> getJsonErrsResp (Object object) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("status", "error");
result.put("message", object);
return ok()
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromPublisher(Mono.just(toJSON(result)), String.class));
}
private String toJSON(Object object) {
ObjectMapper objectMapper = new ObjectMapper();
return Unchecked.function(objectMapper::writeValueAsString).apply(object);
}
}
Repo here
This is where the code fails
#Component
public class RequestHandler {
private final Validator validator;
public RequestHandler(Validator validator) {
this.validator = validator;
}
public <BODY> Mono<ServerResponse> requireValidBody(
Function<Mono<BODY>, Mono<ServerResponse>> block,
ServerRequest request, Class<BODY> bodyClass) {
return request
.bodyToMono(bodyClass)
.flatMap(
body -> validator.validate(body).isEmpty()
? block.apply(Mono.just(body))
: ServerResponse.unprocessableEntity().build()
);
}
}
This is the repo where I found this interesting solution that I want to implement (the bit of code where it's failing)
https://github.com/jeroenbellen/Validate-functional-endpoints-in-Spring/blob/master/src/main/java/foo/bar/springfunctionalwebvalidation/controller/RequestHandler.java
Thank you in advance.
EDIT
public <BODY> Mono<ServerResponse> requireValidBody(
Function<Mono<BODY>, Mono<ServerResponse>> block,
ServerRequest request, Class<BODY> bodyClass) {
Hooks.onOperatorDebug();
return request
.bodyToMono(bodyClass)
.doOnError(Throwable::printStackTrace)
.flatMap(
body -> {
log.info("Emptyness" +validator.validate(body).isEmpty() + "");
return validator.validate(body).isEmpty()
? block.apply(Mono.just(body))
: ServerResponse.unprocessableEntity().build();
}
).doOnError(Throwable::printStackTrace);
doOnError does not out put anything.

Resources