Spring WebFlux : WebClient + Fallback on error - spring

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!

Related

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

Spring cloud gateway with Spring cache and caffeine

I have a spring cloud gateway which forwards the API rest requests to some microservices.
I would like to cache the response for specific requests.
For this reason I wrote this Filter
#Component
#Slf4j
public class CacheResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<CacheResponseGatewayFilterFactory.Config> {
private final CacheManager cacheManager;
public CacheResponseGatewayFilterFactory(CacheManager cacheManager) {
super(CacheResponseGatewayFilterFactory.Config.class);
this.cacheManager = cacheManager;
}
#Override
public GatewayFilter apply(CacheResponseGatewayFilterFactory.Config config) {
final var cache = cacheManager.getCache("MyCache");
return (exchange, chain) -> {
final var path = exchange.getRequest().getPath();
if (nonNull(cache.get(path))) {
log.info("Return cached response for request: {}", path);
final var response = cache.get(path, ServerHttpResponse.class);
final var mutatedExchange = exchange.mutate().response(response).build();
return mutatedExchange.getResponse().setComplete();
}
return chain.filter(exchange).doOnSuccess(aVoid -> {
cache.put(path, exchange.getResponse());
});
};
}
When I call my rest endpoint, the first time I receive the right json, the second time I got an empty body.
What am I doing wrong?
EDIT
This is a screenshot of the exchange.getRequest() just before doing cache.put()
I solved it creating a GlobalFilter and a ServerHttpResponseDecorator. This code is caching all the responses regardless (it can be easily improved to cache only specific responses).
This is the code. However I think it can be improved. In case let me know.
#Slf4j
#Component
public class CacheFilter implements GlobalFilter, Ordered {
private final CacheManager cacheManager;
public CacheFilter(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
final var cache = cacheManager.getCache("MyCache");
final var cachedRequest = getCachedRequest(exchange.getRequest());
if (nonNull(cache.get(cachedRequest))) {
log.info("Return cached response for request: {}", cachedRequest);
final var cachedResponse = cache.get(cachedRequest, CachedResponse.class);
final var serverHttpResponse = exchange.getResponse();
serverHttpResponse.setStatusCode(cachedResponse.httpStatus);
serverHttpResponse.getHeaders().addAll(cachedResponse.headers);
final var buffer = exchange.getResponse().bufferFactory().wrap(cachedResponse.body);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
final var mutatedHttpResponse = getServerHttpResponse(exchange, cache, cachedRequest);
return chain.filter(exchange.mutate().response(mutatedHttpResponse).build());
}
private ServerHttpResponse getServerHttpResponse(ServerWebExchange exchange, Cache cache, CachedRequest cachedRequest) {
final var originalResponse = exchange.getResponse();
final var dataBufferFactory = originalResponse.bufferFactory();
return new ServerHttpResponseDecorator(originalResponse) {
#NonNull
#Override
public Mono<Void> writeWith(#NonNull Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
final var flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(dataBuffers -> {
final var outputStream = new ByteArrayOutputStream();
dataBuffers.forEach(dataBuffer -> {
final var responseContent = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(responseContent);
try {
outputStream.write(responseContent);
} catch (IOException e) {
throw new RuntimeException("Error while reading response stream", e);
}
});
if (Objects.requireNonNull(getStatusCode()).is2xxSuccessful()) {
final var cachedResponse = new CachedResponse(getStatusCode(), getHeaders(), outputStream.toByteArray());
log.debug("Request {} Cached response {}", cacheKey.getPath(), new String(cachedResponse.getBody(), UTF_8));
cache.put(cacheKey, cachedResponse);
}
return dataBufferFactory.wrap(outputStream.toByteArray());
}));
}
return super.writeWith(body);
}
};
}
#Override
public int getOrder() {
return -2;
}
private CachedRequest getCachedRequest(ServerHttpRequest request) {
return CachedRequest.builder()
.method(request.getMethod())
.path(request.getPath())
.queryParams(request.getQueryParams())
.build();
}
#Value
#Builder
private static class CachedRequest {
RequestPath path;
HttpMethod method;
MultiValueMap<String, String> queryParams;
}
#Value
private static class CachedResponse {
HttpStatus httpStatus;
HttpHeaders headers;
byte[] body;
}
}

Spring WebClient : Implement Fallback method

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");
}

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.

Spring WS is generating empty SOAP Envelop

I want do call a SOAP service with Spring WS WebServiceTemplate. I have used this very often and it always worked so far. But now I just get an soap envelope with empty body:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body/></SOAP-ENV:Envelope>
I have created the request and response classes with the JAXB Maven Plugin. And the generated source code looks exactly like the services which are working.
Example:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(
name = "startRequest_RequestParameters",
propOrder = {"url"}
)
#XmlRootElement(
name = "startRequest"
)
public class StartRequest {
#XmlElement(
required = true
)
#XmlSchemaType(
name = "anyURI"
)
protected String url;
public StartRequest() {
}
public String getUrl() {
return this.url;
}
public void setUrl(String value) {
this.url= value;
}
}
I call the webservice template with marshallSendAndReceive
StartRequest request = new StartRequest();
request.setUrl(url);
StartResponse response = (StartResponse) webServiceTemplate.marshalSendAndReceive(endpointUrl, request);
I configure the WebServiceTemplate with java configuration:
public WebServiceTemplate startRequestWebServiceTemplate() throws Exception {
return createWebServiceTemplate(createMarshaller(), createSecurityInterceptor(username, password), createMessageSender(proxyHost, proxyPort));
}
private WebServiceTemplate createWebServiceTemplate(Jaxb2Marshaller marshaller, ClientInterceptor securityInterceptor, WebServiceMessageSender messageSender) {
WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
webServiceTemplate.setMarshaller(marshaller);
webServiceTemplate.setUnmarshaller(marshaller);
webServiceTemplate.setMessageSender(messageSender);
if (securityInterceptor != null) {
webServiceTemplate.setInterceptors((ClientInterceptor[]) Arrays.asList(securityInterceptor, createLoggingInterceptor()).toArray());
} else {
webServiceTemplate.setInterceptors((ClientInterceptor[]) Arrays.asList(createLoggingInterceptor()).toArray());
}
webServiceTemplate.setCheckConnectionForFault(true);
webServiceTemplate.afterPropertiesSet();
return webServiceTemplate;
}
private Jaxb2Marshaller createMarshaller() throws Exception {
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(StartRequest.class, StartResponse.class);
jaxb2Marshaller.afterPropertiesSet();
return jaxb2Marshaller;
}
private ClientInterceptor createLoggingInterceptor() {
return new SoapLoggingInterceptor(systemName);
}
private Wss4jSecurityInterceptor createSecurityInterceptor(String username, String password) {
Wss4jSecurityInterceptor wss4jSecurityInterceptor = new Wss4jSecurityInterceptor();
wss4jSecurityInterceptor.setSecurementPasswordType("PasswordText");
wss4jSecurityInterceptor.setSecurementActions("UsernameToken");
wss4jSecurityInterceptor.setSecurementUsername(username);
wss4jSecurityInterceptor.setSecurementPassword(password);
wss4jSecurityInterceptor.setSkipValidationIfNoHeaderPresent(true);
wss4jSecurityInterceptor.setValidateRequest(false);
wss4jSecurityInterceptor.setValidateResponse(false);
return wss4jSecurityInterceptor;
}
private HttpComponentsMessageSender createMessageSender(String proxyHost, String proxyPort) {
HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender(createHttpClient(proxyHost, proxyPort));
httpComponentsMessageSender.setAcceptGzipEncoding(true);
return httpComponentsMessageSender;
}
private HttpClient createHttpClient(String proxyHost, String proxyPort) {
RequestConfig.Builder configBuilder = RequestConfig.custom()
.setConnectTimeout(DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS)
.setSocketTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS)
.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT);
addProxySettings(configBuilder, proxyHost, proxyPort);
HttpClientBuilder clientBuilder = HttpClients.custom().setDefaultRequestConfig(configBuilder.build());
addInterceptor(clientBuilder);
addConnectionManager(clientBuilder);
return clientBuilder.build();
}
private void addProxySettings(RequestConfig.Builder configBuilder, String proxyHost, String proxyPort) {
if (StringUtils.isNotBlank(proxyHost)) {
configBuilder.setProxy(new HttpHost(proxyHost, Integer.valueOf(proxyPort)));
}
}
private void addInterceptor(HttpClientBuilder clientBuilder) {
clientBuilder.addInterceptorFirst(new HttpComponentsMessageSender.RemoveSoapHeadersInterceptor());
}
private void addConnectionManager(HttpClientBuilder clientBuilder) {
if (maxConnections > DEFAULT_MAX_CONNECTIONS_PER_ROUTE) {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(maxConnections);
cm.setDefaultMaxPerRoute(maxConnections);
clientBuilder.setConnectionManager(cm);
}
}
This configuration worked fine for other soap implementations. But here I just get the soap envelope with the empty body.
Has anyone an idea what's wrong here?
I did something wrong when refactoring the LoggingInterceptor. When handling the request it took the response part from the MessageContext instead of the request part, which caused to overwrite the request with the response. So if you have such a problem check your interceptors if they handle response and request correctly

Resources