Handling exception in WebClient throws io.netty.handler.timeout.ReadTimeoutException - java

So I am new to Reactive programming and I wrote some code that I would like to test. Those are more of a integration tests as I am live copying files and later check if they are the same. I have a MockWebServer mocking my reponse to be 4xx which is handled well in the code. Unfortunately, I am also getting io.netty.handler.timeout.ReadTimeoutException which covers up my custom WebClientResponseException so in the test I am getting the wrong exception. Basically I have two questions, why on earth am I getting this io.netty.handler.timeout.ReadTimeoutException exception? It appears only after doOnError() method for some reason and I am not sure why is it even happening.
Right now code is at it is and it's being synchronous, I am well aware of that.
Second question is, how could I handle my custom exception in the tests after a given number of retries? Right now it is 3 and only then I would like my other exception to be thrown.
Here is the code:
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(targetPath, StandardOpenOption.WRITE);
Flux<DataBuffer> fileDataStream = Mono.just(filePath)
.map(file -> targetPath.toFile().exists() ? targetPath.toFile().length() : 0)
.map(bytes -> webClient
.get()
.uri(uri)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.header("Range", String.format("bytes=%d-", bytes))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new CustomException("4xx error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new CustomException("5xx error")))
.bodyToFlux(DataBuffer.class)
.doOnError(throwable -> log.info("fileDataStream onError", throwable))
)
.flatMapMany(Function.identity());
return DataBufferUtils
.write(fileDataStream, fileChannel)
.map(DataBufferUtils::release)
.doOnError(throwable -> {
try {
fileChannel.force(true);
} catch (IOException e) {
throw new WritingException("failed force update to file channel", e);
}
})
.retry(3)
.doOnComplete(() -> {
try {
fileChannel.force(true);
} catch (IOException e) {
log.warn("failed force update to file channel", e);
throw new WritingException("failed force update to file channel", e);
}
})
.doOnError(throwable -> targetPath.toFile().delete())
.then(Mono.just(target));
The response is Mono<Path> as I am interested only in the Path of the newly created and copied file.
Any comments regarding code are welcome.
The copying mechanism was made basing on this thread Downlolad and save file from ClientRequest using ExchangeFunction in Project Reactor

So basically the problem was in tests. I had a MockResponse enqueued to the MockWebServer only once so upon retrying in the WebClient mocked server did not have any response set (basically it behaved like it is not available at all as there was no mocked response).
To be able to handle exceptions in case of the server being completely down it is in my opinion worth adding lines that goes something like this into your flux chain:
.doOnError(ChannelException.class, e -> {
throw new YourCustomExceptionForHandlingServerIsDownSituation("Server is unreachable", e);
})
This will help you handle ReadTimeoutException from Netty (in case of server being not reachable) as it extends ChannelException class. Always handle your exceptions.

Related

Flux consumer doesnt stop to consume data

I have this implementation
#Override
public Flux<byte[]> translateData(final String datasetId) {
return keyVaultRepository.findByDatasetId(datasetId)
.map(keyVault -> {
try {
return translatorService.createTranslator(keyVault.getKey()); // throws CryptoException in the test
} catch (CryptoException e) {
throw Exceptions.propagate(new ApiException("Unable to provide translated file"));
}
})
.flatMapMany(translator -> storageService.getEntry(datasetId).map(translator::update));
}
and this failing test
#Test
void getTranslatedDataWithError() throws StorageException, CryptoException {
final List<byte[]> bytes = new ArrayList<>();
// exec + validate
getWebTestClient()
.get()
.uri(uriBuilder -> uriBuilder.path("/{datasetId}").build(datasetId))
.exchange()
.expectStatus().is5xxServerError()
.returnResult(byte[].class)
.getResponseBody()
.onErrorStop()
.subscribe(bytes::add);
assertThat(bytes).isEmpty();
}
the part .is5xxServerError() is succeeding but the list is not empty.
The Microservice which is calling the endpoint of translateData should not consume any data from upstream but apparently this is the case.
I've found a workaround by throwing a RuntimeException in the catch block (I could also make the CryptoException unchecked but thats not the matter of my question) and then handle the case in my ControllerAdvice/GlobalExceptionhandler and just return a ResponseEntity with an ErrorDto .
The core of my question is, how can I do this natively with Flux. So that the consumer of the endpoint notices there is an error and .subscribe(bytes::add); wont be even executed.
I have tried it already with .doOnError , .onErrorResume etc. but it always ends with a non empty list :(
By this I am afraid that the bytes will later delivered to the client (which should not of course he should get an error response)

How can I handle a bytearray message in a Spring Integration Flow

I'm having some issues with Spring Integration.
I have an error channel that is supposed to consume an error dedicated queue in case other flows have an error, this handler allows us to log some important data in a very specific format, and then the message is discarded.
The problem is, that this flow is configured to receive a specific message type, like a FailedMessage class.
Whenever the message is consumed, I get a class cast exception saying that [B cannot be cast to FailedMessage.
So, after doing some research I implemented the following transformer:
private FailedMessage parseFailedMessage(byte[] genericMessage){
try {
ByteArrayInputStream in = new ByteArrayInputStream(genericMessage);
ObjectInputStream is = new ObjectInputStream(in);
return (FailedMessage) is.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("An unexpected error occurred parsing FailedMessage", e);
}
}
And the IntegrationFlow spec is :
#Bean
public IntegrationFlow errorHandlingFlow(XmlMessageTransformer transformer) {
return IntegrationFlows.from("ErrorChannel")
.transform(this::parseFailedMessage)
.<FailedMessage>handle((p, h) -> {
processFailedMessage(p, transformer);
return p;
})
.channel("discard")
.get();
}
Is this an acceptable way to handle this kind of message or there's a way to automatize the transform step?

Use flatMap after method returning Mono<Void>

I'm stuck with understanding of some aspects of Project Reactor.
I have the following list of components:
Validator of input params, returns Mono<Void> or Mono.error()
Service saving data to db, returns Mono<Item>
Logger for successful actions of an user, returns Mono<Void
A business logic is quite simple: validate params (1), save an item to db (2) and log actions (3). The problem is validator (1) returns Mono.empty() if there are no errors with input data and Mono.error() if input params contain some errors.
I would like to achieve the next things:
If validator returns Mono.empty() then continue chain
If validator returns Mono.error() then immediately stop processing and throw error which will be handled by exceptionHanlder
I have tried two options:
First with .then(Mono<Item> item) after validation. It allows me to execute saving operation after validation. Given that .then() ignores any errors, I can't rise an exception.
return inputValidator.validateFields(userId, projectId)
.then(repository.save(item))
.onErrorMap(RepoException.class, ex -> new UnexpectedError("Failed to save item", ex))
.subscribeOn(Schedulers.boundedElastic())
.doOnSuccess(n -> logService.logActivity(new Activity(adminId, n))
.subscribe());
Second with .flatMap(Function<Mono<Void>, <Mono<? extends Item> func) after validation. This approach can rise an exception from validator, but I can't execute saving operation because flatMap() doesn't trigger on empty result.
return inputValidator.validateFields(userId, projectId)
.flatMap(v -> repository.save(item))
.onErrorMap(RepoException.class, ex -> new UnexpectedError("Failed to save item", ex))
.subscribeOn(Schedulers.boundedElastic())
.doOnSuccess(n -> logService.logActivity(new Activity(adminId, n))
.subscribe());
It is also important to have access for created object after saving (step 2), because I need to pass it to logger service.
You can't use flatMap because there is no onNext signal - use then instead. Not sure what do you mean by "called" but there is a difference between Assembly and Subscription time in reactive. Publisher you specified in then will not be resolved in case inputValidator.validateFields returns onError signal.
Here is a test for failed validation and as you may see subscription was not triggered
#Test
void saveWasNotCalledIfValidationFailed() {
var saveMock = PublisherProbe.of(Mono.just("id"));
var repo = mock(Repository.class);
when(repo.save())
.thenReturn(saveMock.mono());
var res = validateFields()
.then(repo.save())
.onErrorMap(IllegalArgumentException.class,
ex -> new IllegalStateException("Failed to save item", ex)
);
StepVerifier.create(res)
.expectError(IllegalStateException.class)
.verify();
saveMock.assertWasNotSubscribed();
}
private Mono<Void> validateFields() {
return Mono.error(new IllegalArgumentException("oops"));
}
public static class Repository {
public Mono<String> save() {
return Mono.just("id");
}
}
and here is a test for passed validation
#Test
void saveIsCalledIfValidationPassed() {
var saveMock = PublisherProbe.of(Mono.just("id"));
var repo = mock(Repository.class);
when(repo.save())
.thenReturn(saveMock.mono());
var res = validateFields()
.then(repo.save())
.onErrorMap(IllegalArgumentException.class,
ex -> new IllegalStateException("Failed to save item", ex)
);
StepVerifier.create(res)
.expectNext("id")
.verifyComplete();
saveMock.assertWasSubscribed();
}
private Mono<Void> validateFields() {
return Mono.empty();
}

nesting synchron and asynchron spring webclient requests without block()

i wrote a little piece of code as PoC to see if i can get that running. it should be a client for a website (oldschool chat system). unfortunately, they have some "special" handling of their website, e.g. frames and some calculations in .js that must be done before i can access the stream.
public void run() {
final URI chatStreamUri = getUserStreamUrl();
String block = webClient
.get()
.uri(chatStreamUri)
.retrieve()
.bodyToMono(String.class)
.map(s -> transformToBlah(s, chatAppUser.getInternalPass()))
.flatMap(s -> webClient.get().uri(s).retrieve().bodyToMono(String.class))
.map(this::getCrapStreamUrl)
.block();
System.out.println("block url: " + block);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
webClient
.get()
.uri(block)
.retrieve()
.bodyToFlux(String.class)
.subscribe(writeTo,
new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) {
System.out.println(throwable);
}
}
);
}
unfortunately that throws the error in the sysout(throwable):
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-2
I only have the .block() in the synchronous part so i assume it is because of that.
the steps that must happen are:
make a GET on the streamUrl (i guess that sets some cookies or so).
that response contains some wierdo looking url like http://example.com/user/+pass+/stream, where i replace that +pass+ with the password of that user
make a GET on the http://example.com/user/$verySecret/stream url to get the token - that is somehow generated on server side, so the response looks like: "the stream url is: http://example.com/user/24987624/blabla/stream"
and then listen on that http://example.com/user/24987624/blabla/stream for content
all those calls must be done on the same client object, i dont know why but when i access the stream url directly, it will fail (guess some cookie stuff).
anyone an idea how to achieve that with springs reactive webclient? or should i consider something else? maybe its not the correct library to use. thought i take a look at that fancy new reactive stuff ;)
btw.: when i debug, it works. thats why i added that ugly Thread.sleep() which unfortunately did not workaround the problem.
edit:
when i try it with this (as suggested i guess) it does not work:
webClient
.get()
.uri(chatStreamUri)
.retrieve()
.bodyToFlux(String.class)
.map(s -> transformToBlah(s, chatAppUser.getInternalPass()))
.flatMap(s -> webClient.get().uri(s).retrieve().bodyToMono(String.class))
.map(this::getCrapStreamUrl)
.subscribe(writeTo, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) {
System.out.println(throwable);
}
});
then i only receive a message like:
java.lang.RuntimeException: got but cant work with that
this is from one of my methods that expect not a <html> but a text block containing way more text (like a window.replace code snippet -> thats where i filter out the next url i have to access).
when i debug that and set a breakpoint, i really only retrieve a <html> tag, nothing more
edit #2: when i use bodyToMono instead i get more stuff (makes sense now when i think about it), but still, the methods are not called as expected. Now the first method is called (transformToBlah()), then the second method is called which extracts the stream url, but then there is no subscribe on that extracted stream url, what happens is that the subscriber retrieves the stream url. thats not what i expected. for me, it looks like i have to now somehow call again webclient.get().uri($theResultFromMapThis::getCrapStreamUrl).subscribe(writeTo);
what does also not work is something like:
webClient
.get()
.uri(chatStreamUri)
.retrieve()
.bodyToMono(String.class)
.map(s -> transformToBlah(s, chatAppUser.getInternalPass()))
.flatMap(s -> webClient.get().uri(s).retrieve().bodyToMono(String.class))
.map(this::getCrapStreamUrl)
.flatMap(s -> webClient.get().uri(s).retrieve().bodyToMono(String.class))
.subscribe(writeTo, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) {
System.out.println(throwable);
}
});
edit #3: what a heck of a ride. here is the code that works (a while):
webClient
.get()
.uri(chatStreamUri)
.retrieve()
.bodyToMono(String.class)
.flatMap(s -> Mono.just(transformToBlah(s, chatAppUser.getInternalPass())))
.flatMap(x -> webClient.get().uri(x).retrieve().bodyToMono(String.class))
.map(this::getCrapStreamUrl)
.flatMapMany(s -> webClient.get().uri(s).retrieve().bodyToFlux(String.class))
.subscribe(writeTo, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) {
System.out.println("err: " + throwable);
}
});
edit #4: i first thought the above code made it, and it works for some time, but then i again retrieve:
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-2 from the System.out.println and no idea why :/
edit #5: seems like those exceptions are from a very different call, that has nothing to do with that part at all. i will open another thread for that. commented that other part/code and this works now as expected for about an hour, so guess the code works. thanks!
i will watch that application now for a couple of hours and if it still does work i close this.

JUnit5 Test coverage for catch block of a Spring Boot Controller

I am trying to write test for my controller with the code below. I want to cover the test for the code in the catch block statement but I'm not able to write one. I want to return a Server Response with failure code and message in the catch block.
#PostMapping(COUNTERS)
public ResponseEntity<?> getCounters(#Valid #RequestBody ApartmentCounterListRequest requestData) {
try {
log.debug("Entering API for counter list");
ApartmentCounterListResponse apartmentCounterListResponse = counterService.getAllCounters();
return ResponseEntity.ok(apartmentCounterListResponse);
} catch (Exception exception) {
log.error("Exception in counter list :: ", exception);
ServerResponse serverResponse = ResponseBuilder.buildVendorFailureMessage(new ServerResponse(),
RequestResponseCode.EXCEPTION);
return ResponseEntity.ok(JsonResponseBuilder.enquiryResponse(serverResponse));
}
}
My test code is as follows:
#Test
#DisplayName("Should return ServerResponse with failure data.")
void Should_Return_Server_Response_On_Exception() throws Exception {
/*given*/
ApartmentCounterListRequest apartmentCounterListRequest = ApartmentTestUtil.getApartmentCounterListRequest.
apply("test", "test");
Mockito.when(counterServic.getAllCounters()).thenThrow(new Exception());
// ServerResponse serverResponse = ApartmentTestUtil.getServerExceptionServerResponse.get();
/*then*/
mockMvc.perform(
post(COUNTER_URL)
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(apartmentCounterListRequest)))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.resultCode", Matchers.is("-6")));
verify(counterService, times(1)).getAllCounters();
}
When I run this test I am getting the following error:
org.mockito.exceptions.base.MockitoException:
Checked exception is invalid for this method!
Invalid: java.lang.Exception
I have gone through some of the following posts but haven't found a suitable answer yet.
Unit testing code in catch block of a Spring Controller
Java - How to Test Catch Block?
Unit testing code in catch block of a Spring Controller
JUnit for both try and catch block coverage
Can anyone help me write test that covers the catch block or tell me the way to do it?
I have this try catch in my controller to handle any unexpected exceptions. And for different api's I have to send a response with different response code and messages which doesnot allow me to use Exception handler.
The method you are mocking does not declare a checked exception, therefore Mockito is not able to throw one from there. Try to have the mock throw an unchecked exception (i.e. RuntimeException).
You can try to use willAnswer
Mockito.when(counterServic.getAllCounters()).thenAnswer(x -> {throw new Exception() });
Maybe this is a bit misused as Answer is used for more complex for when return

Categories

Resources