I'm attempting to create a library that leverages Spring Webclient that has common failure logic for all of our applications. There are 2 considerations - configurable retry logic and failover logic to secondary sites.
The failover needs to handle network exceptions and request exceptions with a specific status code, default >= 500.
Would anyone be able to sanity check the below?
i.e.
public Builder webClientBuilder() throws SSLException {
return WebClient.builder()
.baseUrl(baseUrl)
.filter((request, next) -> exchangeFilterFunction(request, next))
.clientConnector(createReactorClientHttpConnector());
}
private Mono<ClientResponse> exchangeFilterFunction(ClientRequest request, ExchangeFunction next) {
if (StringUtils.hasLength(failoverBaseUrl)) {
return nextExchangeWithRetry(request, next)
.onErrorResume(e -> {
if (isRetriableException(e)) {
String uri = request.url().toString().replace(baseUrl, failoverBaseUrl);
LOGGER.info("Attempting to call Failover Site: " + uri);
ClientRequest retryRequest = ClientRequest.from(request).url(URI.create(uri)).build();
return nextExchangeWithRetry(retryRequest, next);
}
return Mono.error(e);
});
} else {
LOGGER.debug("No Failover Configured");
return nextExchangeWithRetry(request, next);
}
}
private Mono<ClientResponse> nextExchangeWithRetry(ClientRequest request, ExchangeFunction next) {
return next.exchange(request)
.flatMap(clientResponse -> {
HttpStatus httpStatus = clientResponse.statusCode();
if (httpStatus.isError()) {
return clientResponse.createException().flatMap(Mono::error);
}
return Mono.just(clientResponse);
})
.retryWhen(retrySpec(request));
}
private boolean isRetriableException(Throwable throwable) {
if (nonRetriableExceptions != null && nonRetriableExceptions.contains(throwable.getClass())) {
return false;
}
if (throwable instanceof WebClientResponseException
&& isNotRetryHttpResponseCode(((WebClientResponseException) throwable).getRawStatusCode())) {
return false;
}
return true;
}
Related
Is there any way to handle exceptions in a Flux parallel in case N of X rails fail? I have tried with the onErrorMap, onErrorReturn, and with this try catch, but it keeps throwing error even if all the others are ok, because it is going to the catch of the processRequest method.
protected Object processRequest(RequestHolder requestHolder) {
RequestHolderImpl requestHolderImpl = (RequestHolderImpl) requestHolder;
try {
if (requestHolderImpl.getPayload().getClass().equals(LinkedList.class)) {
payload.addAll((List<DataSourceRequest>) requestHolderImpl.getPayload());
} else {
payload.add((DataSourceRequest) requestHolderImpl.getPayload());
}
List<PurposeResponse> response = Flux.fromIterable(payload)
.parallel()
.flatMap(request -> {
try {
return dataSourceCall(request);
} catch (WebClientResponseException e) {
return Mono.just(new PurposeResponse(request.getPurpose(), buildResponseFromException(e, request.getPurpose())));
} catch (Exception e) {
LOGGER.error("No response could be obtained from DS. Exception thrown: {}", e.getMessage());
return Mono.just(new PurposeResponse(request.getPurpose(), new DataSourceException(e)));
}
})
.sequential()
.collectList()
.block();
return new ResponseHolderImpl(response, products);
} catch (Exception e) {
return new DataSourceException(e.getMessage());
}
}
private Mono<PurposeResponse> dataSourceCall(DataSourceRequest purpose) {
RequestHolder requestHolder = new RequestHolderImpl(purpose,
data,
products,
token);
String purposeName = getPurposeName(requestHolder);
RequestEntity<?> requestEntity = createRequestEntity(requestHolder);
LOGGER.info("Sending request to this url: {}", requestEntity.getUrl());
return webClient.get()
.uri(requestEntity.getUrl())
.header("Authorization", "Bearer " + token)
.retrieve()
.bodyToMono(JsonNode.class)
.elapsed()
.map(data -> {
LOGGER.info("Response took {} milliseconds", data.getT1());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Response obtained from Data Source: {}", data.getT2());
}
return new PurposeResponse(purposeName, data.getT2());
});
}
private Object buildResponseFromException(WebClientResponseException e, String purposeName) {
//do things
}
I am receiving a request in a 0.RestController and give it to the service for processing. If service doesnt throw exception, i just return HttpStatus.200, but if an exception occurs in the service, i need catch it in controller and return the status depending on the exception.
Inside service, i need to use Mono.fromCallable for repo access. Well, if user not found, i try throw my CustomException, but i cant cach it in controller. What am I doing wrong?
Controller:
#RestController
#RequiredArgsConstructor
#Slf4j
public class CardStatusController {
private final CardStatusService cardStatusService;
#PostMapping(value = "api/{user_id}/confirm", produces = {MediaType.APPLICATION_JSON_VALUE})
public Mono<ResponseEntity<HttpStatus>> confirmStatus(
#PathVariable("user_id") String userId,
#RequestBody CardStatusRequest statusRequest) {
statusRequest.setUserId(userId);
cardStatusService.checkCardStatus(statusRequest);
return Mono.just(new ResponseEntity<>(HttpStatus.OK));
}
#ExceptionHandler({ CheckCardStatusException.class })
public void handleException() {
log.error("error!");
}
My service:
public Mono<Void> checkCardStatus(CardStatusRequest statusRequest) throws CheckCardStatusException {
if (statusRequest.getCardEmissionStatus().equals(CardEmissionStatus.ACCEPTED)) {
String reference = statusRequest.getReference();
return Mono.fromCallable(() -> userRepository.findById(statusRequest.getUserId()))
.subscribeOn(Schedulers.boundedElastic())
.switchIfEmpty(Mono.error(new CheckCardStatusException(HttpStatus.NOT_FOUND)))
.flatMap(user -> Mono.fromCallable(() -> cardRepository.findFireCardByUserId(user.getId()))
.flatMap(optionalCard -> {
if (optionalCard.isPresent()) {
if (optionalCard.get().getExtId().isEmpty()) {
Card card = optionalCard.get();
card.setExtId(reference);
try {
cardRepository.save(card);
} catch (Exception e) {
return Mono.error(new CheckCardStatusException(HttpStatus.INTERNAL_SERVER_ERROR));
}
} else {
if (!optionalCard.get().getExtId().equals(reference)) {
return Mono.error(new CheckCardStatusException(HttpStatus.CONFLICT));
}
}
} else {
Card card = new Card();
//set card params
try {
cardRepository.save(card);
} catch (Exception e) {
return Mono.error(new CheckCardStatusException(HttpStatus.INTERNAL_SERVER_ERROR));
}
}
return Mono.error(new CheckCardStatusException(HttpStatus.OK));
})).then();
}
else {
return Mono.error(new CheckCardStatusException(HttpStatus.OK));
}
}
}
My CustomException:
#AllArgsConstructor
#Getter
public class CheckCardStatusException extends RuntimeException {
private HttpStatus httpStatus;
}
The Mono returned by checkCardStatus is never subscribed, so the error signal is ignored. You have to return the whole chain to the Webflux framework as follows:
public Mono<ResponseEntity<HttpStatus>> confirmStatus(
#PathVariable("user_id") String userId,
#RequestBody CardStatusRequest statusRequest) {
statusRequest.setUserId(userId);
return cardStatusService.checkCardStatus(statusRequest)
.then(Mono.just(new ResponseEntity<>(HttpStatus.OK)));
}
In case of an error, the corresponding ExceptionHandler will be executed.
The fact is that I need to simultaneously pull in data from the local database, from the server, while checking the connection to the Internet.
Without checking the internet is easy. But when I turn off mobile data, crashes.
I do not understand how to combine and decided to do this:
private void getCategories() {
composite.add(getDataFromLocal(context)
.observeOn(AndroidSchedulers.mainThread()).flatMap(new Function<PromoFilterResponse, ObservableSource<List<FilterCategory>>>() {
#Override
public ObservableSource<List<FilterCategory>> apply(PromoFilterResponse promoFilterResponse) throws Exception {
if (promoFilterResponse != null) {
PreferencesHelper.putObject(context, PreferencesKey.FILTER_CATEGORIES_KEY, promoFilterResponse);
return combineDuplicatedCategories(promoFilterResponse);
} else {
return Observable.empty();
}
}
})
.subscribe(new Consumer<List<FilterCategory>>() {
#Override
public void accept(List<FilterCategory> categories) throws Exception {
if (mView != null) {
mView.hideConnectingProgress();
if (categories != null && categories.size() > 0) {
mView.onCategoriesReceived(categories);
}
}
}
}));
composite.add(InternetUtil.isConnectionAvailable().subscribe(isOnline -> {
if (isOnline) {
composite.add(
getDataFromServer(context)
.flatMap(new Function<PromoFilterResponse, ObservableSource<List<FilterCategory>>>() {
#Override
public ObservableSource<List<FilterCategory>> apply(PromoFilterResponse promoFilterResponse) throws Exception {
if (promoFilterResponse != null) {
PreferencesHelper.putObject(context, PreferencesKey.FILTER_CATEGORIES_KEY, promoFilterResponse);
return combineDuplicatedCategories(promoFilterResponse);
} else {
return Observable.empty();
}
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(categories -> {
if (mView != null) {
mView.hideConnectingProgress();
if (categories != null && categories.size() > 0) {
mView.onCategoriesReceived(categories);
} else {
mView.onCategoriesReceivingFailure(errorMessage[0]);
}
}
}, throwable -> {
if (mView != null) {
if (throwable instanceof HttpException) {
ResponseBody body = ((HttpException) throwable).response().errorBody();
if (body != null) {
errorMessage[0] = body.string();
}
}
mView.hideConnectingProgress();
mView.onCategoriesReceivingFailure(errorMessage[0]);
}
}));
} else {
mView.hideConnectingProgress();
mView.showOfflineMessage();
}
}));
}
private Single<Boolean> checkNetwork(Context context) {
return InternetUtil.isConnectionAvailable()
.subscribeOn(Schedulers.io())
.doOnSuccess(new Consumer<Boolean>() {
#Override
public void accept(Boolean aBoolean) throws Exception {
getDataFromServer(context);
}
});
}
private Observable<PromoFilterResponse> getDataFromServer(Context context) {
return RetrofitHelper.getApiService()
.getFilterCategories(Constants.PROMO_FILTER_CATEGORIES_URL)
.subscribeOn(Schedulers.io())
.retryWhen(BaseDataManager.isAuthException())
.publish(networkResponse -> Observable.merge(networkResponse, getDataFromLocal(context).takeUntil(networkResponse)))
.doOnNext(new Consumer<PromoFilterResponse>() {
#Override
public void accept(PromoFilterResponse promoFilterResponse) throws Exception {
PreferencesHelper.putObject(context, PreferencesKey.FILTER_CATEGORIES_KEY, promoFilterResponse);
}
})
.doOnError(new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
LogUtil.e("ERROR", throwable.getMessage());
}
});
}
private Observable<PromoFilterResponse> getDataFromLocal(Context context) {
PromoFilterResponse response = PreferencesHelper.getObject(context, PreferencesKey.FILTER_CATEGORIES_KEY, PromoFilterResponse.class);
if (response != null) {
return Observable.just(response)
.subscribeOn(Schedulers.io());
} else {
return Observable.empty();
}
}
As you can see, connect the local database separately, simultaneously check the Internet and upload data from the server.
But it seems to me not quite right. Moreover, the subscriber is duplicated and so on.
I saw a lot of tutorials, where the combination of the local database with the API is described, but I did not see it at the same time processing the connection error with the Internet.
I think many people faced such a problem and how did you solve it?
Suppose You have two Obsevable: one from server and another from database
You can merge them into one stream like below:
public Observable<Joke> getAllJokes() {
Observable<Joke> remote = mRepository.getAllJokes()
.subscribeOn(Schedulers.io());
Observable<Joke> local = mRepository.getAllJokes().subscribeOn(Schedulers.io());
return Observable.mergeDelayError(local, remote).filter(joke -> joke != null);
}
Im' not android developer, but in my mind methods return types should be something like this:
//just for demonstration
static boolean isOnline = false;
static class NoInternet extends RuntimeException {
}
private static Completable ensureOnline() {
if (isOnline)
return Completable.complete();
else
return Completable.error(new NoInternet());
}
private static Single<String> getDataFromServer() {
return Single.just("From server");
}
private static Maybe<String> getDataFromLocal() {
return Maybe.just("From local");//or Maybe.never()
}
We can run all in parallel with Observable.merge. But what if error NoIternet happens? Merged observable will fail. We can use materialisation - transform all emission and errors to onNext value.
private static void loadData() {
Observable<Notification<String>> fromServer = ensureOnline().andThen(getDataFromServer()).toObservable().materialize();
Observable<Notification<String>> fromLocaldb = getDataFromLocal().toObservable().materialize();
Observable.merge(fromLocaldb, fromServer)
.subscribe(notification -> {
if (notification.isOnNext()) {
//calls one or two times(db+server || db || server)
//show data in ui
} else if (notification.isOnError()) {
if (notification.getError() instanceof NoInternet) {
//show no internet
} else {
//show another error
}
} else if (notification.isOnComplete()){
//hide progress bar
}
});
}
This is my current setup:
queue1 and queue2 are marged together with integration flow to channel1:
#Bean
public IntegrationFlow q1f() {
return IntegrationFlows
.from(queue1InboundAdapter())
...
.channel(amqpInputChannel())
.get();
}
#Bean
public IntegrationFlow q2f() {
return IntegrationFlows
.from(queue2InboundAdapter())
...
.channel(amqpInputChannel())
.get();
}
then, everything is aggregated and then confirmed after aggregated message is confirmed by rabbitmq:
#Bean
public IntegrationFlow aggregatingFlow() {
return IntegrationFlows
.from(amqpInputChannel())
.aggregate(...
.expireGroupsUponCompletion(true)
.sendPartialResultOnExpiry(true)
.groupTimeout(TimeUnit.SECONDS.toMillis(10))
.releaseStrategy(new TimeoutCountSequenceSizeReleaseStrategy(200, TimeUnit.SECONDS.toMillis(10)))
)
.handle(amqpOutboundEndpoint())
.get();
}
#Bean
public AmqpOutboundEndpoint amqpOutboundEndpoint() {
AmqpOutboundEndpoint outboundEndpoint = new AmqpOutboundEndpoint(ackTemplate());
outboundEndpoint.setConfirmAckChannel(manualAckChannel());
outboundEndpoint.setConfirmCorrelationExpressionString("#root");
outboundEndpoint.setExchangeName(RABBIT_PREFIX + "ix.archiveupdate");
outboundEndpoint.setRoutingKeyExpression(routingKeyExpression()); //forward using patition id as routing key
return outboundEndpoint;
}
ackTemplate() is set with cf that has springFactory.setPublisherConfirms(true);.
The problem I see is that once in 10 days, there are some messages that are stuck in unacknowledged state in rabbitmq.
My guess is that somehow publish of message is waiting for rabbit to do PUBLISHER CONFIRMS but it never gets it and times out? In this case, I never ACK message in queue1. Is this possible?
So just one more time complete workflow:
[two queues -> direct channel -> aggregator (keeps channel and tag values) -> publish to rabbit -> rabbit returns ACK via publisher confirms -> spring confirms all messages on channel+values that it kept in memory for aggregated message]
I also have my implementation of aggregator (since I need to manually ack messages from both q1 and q2):
public abstract class AbstractManualAckAggregatingMessageGroupProcessor extends AbstractAggregatingMessageGroupProcessor {
public static final String MANUAL_ACK_PAIRS = PREFIX + "manualAckPairs";
private AckingState ackingState;
public AbstractManualAckAggregatingMessageGroupProcessor(AckingState ackingState){
this.ackingState = ackingState;
}
#Override
protected Map<String, Object> aggregateHeaders(MessageGroup group) {
Map<String, Object> aggregatedHeaders = super.aggregateHeaders(group);
List<ManualAckPair> manualAckPairs = new ArrayList<>();
group.getMessages().forEach(m -> {
Channel channel = (Channel)m.getHeaders().get(AmqpHeaders.CHANNEL);
Long deliveryTag = (Long)m.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
manualAckPairs.add(new ManualAckPair(channel, deliveryTag, ackingState));
});
aggregatedHeaders.put(MANUAL_ACK_PAIRS, manualAckPairs);
return aggregatedHeaders;
}
}
UPDATE
This is how rabbit admin looks (2 unacked messages for a long time, and it will not be ACKED untill restart - when it is redelivered):
In Spring AMQP version 2.1 (Spring Integration 5.1), We added a Future<?> and returned message to the CorrelationData to assist with this kind of thing. If you are using an older version, you can subclass CorrelationData (and you'd have to handle setting the future and returned message in your code).
This, together with a scheduled task, can detect missing acks...
#SpringBootApplication
#EnableScheduling
public class Igh2755Application {
public static void main(String[] args) {
SpringApplication.run(Igh2755Application.class, args);
}
private final BlockingQueue<CorrelationData> futures = new LinkedBlockingQueue<>();
#Bean
public ApplicationRunner runner(RabbitTemplate template) {
return args -> {
SuccessCallback<? super Confirm> successCallback = confirm -> {
System.out.println((confirm.isAck() ? "A" : "Na") + "ck received");
};
FailureCallback failureCallback = throwable -> {
System.out.println(throwable.getMessage());
};
// Good - ack
CorrelationData correlationData = new CorrelationData("good");
correlationData.getFuture().addCallback(successCallback, failureCallback);
this.futures.put(correlationData);
template.convertAndSend("", "foo", "data", correlationData);
// Missing exchange nack, no return
correlationData = new CorrelationData("missing exchange");
correlationData.getFuture().addCallback(successCallback, failureCallback);
this.futures.put(correlationData);
template.convertAndSend("missing exchange", "foo", "data", correlationData);
// Missing queue ack, with return
correlationData = new CorrelationData("missing queue");
correlationData.getFuture().addCallback(successCallback, failureCallback);
this.futures.put(correlationData);
template.convertAndSend("", "missing queue", "data", correlationData);
};
}
#Scheduled(fixedDelay = 5_000)
public void checkForMissingAcks() {
System.out.println("Checking pending acks");
CorrelationData correlationData = this.futures.poll();
while (correlationData != null) {
try {
if (correlationData.getFuture().get(10, TimeUnit.SECONDS).isAck()) {
if (correlationData.getReturnedMessage() == null) {
System.out.println("Ack received OK for " + correlationData.getId());
}
else {
System.out.println("Message returned for " + correlationData.getId());
}
}
else {
System.out.println("Nack received for " + correlationData.getId());
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Interrupted");
}
catch (ExecutionException e) {
System.out.println("Failed to get an ack " + e.getCause().getMessage());
}
catch (TimeoutException e) {
System.out.println("Timed out waiting for ack for " + correlationData.getId());
}
correlationData = this.futures.poll();
}
System.out.println("No pending acks, exiting");
}
}
.
Checking pending acks
Ack received OK for good
Nack received for missing exchange
Message returned for missing queue
No pending acks, exiting
With Spring Integration there is a confirmCorrelationExpression which can be used to create the CorrelationData instance.
EDIT
With Spring Integration...
#SpringBootApplication
#EnableScheduling
public class Igh2755Application {
public static void main(String[] args) {
SpringApplication.run(Igh2755Application.class, args);
}
private final BlockingQueue<CorrelationData> futures = new LinkedBlockingQueue<>();
public interface Gate {
void send(#Header("exchange") String exchange, #Header("rk") String rk, String payload);
}
#Bean
#DependsOn("flow")
public ApplicationRunner runner(Gate gate) {
return args -> {
gate.send("", "foo", "good");
gate.send("junque", "rk", "missing exchange");
gate.send("", "junque", "missing queue");
};
}
#Bean
public IntegrationFlow flow(RabbitTemplate template) {
return IntegrationFlows.from(Gate.class)
.handle(Amqp.outboundAdapter(template)
.confirmCorrelationExpression("#correlationCreator.create(#root)")
.exchangeNameExpression("headers.exchange")
.routingKeyExpression("headers.rk")
.returnChannel(returns())
.confirmAckChannel(acks())
.confirmNackChannel(acks()))
.get();
}
#Bean
public MessageChannel acks() {
return new DirectChannel();
}
#Bean
public MessageChannel returns() {
return new DirectChannel();
}
#Bean
public IntegrationFlow ackFlow() {
return IntegrationFlows.from("acks")
/*
* Work around a bug because the correlation data is wrapped and so the
* wrong future is completed.
*/
.handle(m -> {
System.out.println(m);
if (m instanceof ErrorMessage) { // NACK
NackedAmqpMessageException nme = (NackedAmqpMessageException) m.getPayload();
CorrelationData correlationData = (CorrelationData) nme.getCorrelationData();
correlationData.getFuture().set(new Confirm(false, "Message was returned"));
}
else {
((CorrelationData) m.getPayload()).getFuture().set(new Confirm(true, null));
}
})
.get();
}
#Bean
public IntegrationFlow retFlow() {
return IntegrationFlows.from("returns")
.handle(System.out::println)
.get();
}
#Bean
public CorrelationCreator correlationCreator() {
return new CorrelationCreator(this.futures);
}
public static class CorrelationCreator {
private final BlockingQueue<CorrelationData> futures;
public CorrelationCreator(BlockingQueue<CorrelationData> futures) {
this.futures = futures;
}
public CorrelationData create(Message<String> message) {
CorrelationData data = new CorrelationData(message.getPayload());
this.futures.add(data);
return data;
}
}
#Scheduled(fixedDelay = 5_000)
public void checkForMissingAcks() {
System.out.println("Checking pending acks");
CorrelationData correlationData = this.futures.poll();
while (correlationData != null) {
try {
if (correlationData.getFuture().get(10, TimeUnit.SECONDS).isAck()) {
if (correlationData.getReturnedMessage() == null
&& !correlationData.getId().equals("Message was returned")) {
System.out.println("Ack received OK for " + correlationData.getId());
}
else {
System.out.println("Message returned for " + correlationData.getId());
}
}
else {
System.out.println("Nack received for " + correlationData.getId());
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Interrupted");
}
catch (ExecutionException e) {
System.out.println("Failed to get an ack " + e.getCause().getMessage());
}
catch (TimeoutException e) {
System.out.println("Timed out waiting for ack for " + correlationData.getId());
}
correlationData = this.futures.poll();
}
System.out.println("No pending acks, exiting");
}
}
you can declare connection as bean
#Bean
public ConnectionFactory createConnectionFactory(){
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("127.0.0.1", 5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherReturns(true);
connectionFactory.setPublisherConfirmType(ConfirmType.SIMPLE);
return connectionFactory;
}
Then RabbitTemplate as
#Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(callback);
return rabbitTemplate;
}
Where callback is implementation of ConfirmCallback interface
and while sending you can just wait for confirmation
System.out.println("Sending message...");
rabbitTemplate.convertAndSend(rabbitMQProperties.getEXCHANGENAME(),
rabbitMQProperties.getQUEUENAME(), "hello from rabbit");
rabbitTemplate.waitForConfirms(1);
waitforconfirms will take time in milisecond. I put it as 1 for testing purpose.
I have a method:
public void getVmsAdminToken(HttpClient httpClient, handler<AsyncResult<String>> handler) {
httpClient.postAbs(url, h -> h.bodyHandler(bh -> {
try {
switch (h.statusCode()) {
case 200:
JsonObject vmsResponse = bh.toJsonObject();
handler.handle(Future.succeededFuture(Json.encode(vmsResponse)));
break;
default:
LOG.error("VMS call failed {}", h.statusCode());
handler.handle(Future.failedFuture(500 + ""));
break;
}
} catch (Throwable t) {
LOG.error("Exception in getVmsAdminToken", t);
handler.handle(Future.failedFuture(500 + ""));
}
}))
.setTimeout(timeOutMs)
.putHeader("content-type", "application/json")
.putHeader("stub", apiKey)
.end(vehicleReqBody.encode());
}
I use this inside the following method call :
private void getAdminToken(RoutingContext ctx, RedisFleetStorage storage, HttpClient httpClient) {
getVmsAdminToken(fleetId, user, vehicle, httpClient, replyVms -> {
if (reply.succeeded()) {
// why succeeded?!!
}
});
}
And even if the getVmsToken fails, the execution falls into the if (reply.succeeded())
Why might that be?
You should check the same AsyncResult object being the result of your HTTP call:
private void getAdminToken(RoutingContext ctx, RedisFleetStorage storage, HttpClient httpClient) {
getVmsAdminToken(fleetId, user, vehicle, httpClient, replyVms -> {
if (replyVms.succeeded()) {
// do you thing
}
});
}