I have a Spring reactive web client used to connect to a web service.
@Autowired
private org.springframework.web.reactive.function.client.WebClient webClient;
public Mono<MyData> getMyData(MyRequest request) {
return webClient
.get()
.uri(uriBuilder -> this.createUri(uriBuilder, request))
// Any exception thrown before this point is outside of the Mono context
.retrieve()
.bodyToMono(MyDataDto.class)
.map(myDataDto -> mapToMyData(myDataDto))
;
}
There is a possibility that the createUri()
method throws an exception:
private URI createUri(UriBuilder uriBuilder, MyRequest request) {
if (somethingIsInconsistent) {
throw new MyException();
}
}
The problem is that the exception is thrown in the main thread when the Mono
is created, and not in the context of the Mono
when it is subscribed.
As a result, the following test fails:
@Test
void getMyData_whenBadRequest_throwExceptionWhenSubscribed() {
service.getMyData(badRequest)
.as(StepVerifier::create)
.expectError(MyException.class)
.verify();
}
and this passes:
@Test
void getMyData_whenBadRequest_throwExceptionWhenCreated() {
assertThrows(MyException.class,
() -> service.getMyData(badRequest));
}
I'd like to catch all the exception in the context of the Mono
subscription.
Is there some nice reactive WebClient
idiom to wrap the call so all exceptions are thrown when the Mono
is subscribed? All I came with is
public Mono<MyData> getMyData(MyRequest request) {
return Mono.defer(() -> webClient
.get()
.uri(uriBuilder -> this.createUri(uriBuilder, req))
.retrieve()
.bodyToMono(MyDataDto.class)
)
.map(myDataDto -> mapToMyData(myDataDto))
;
}
but this looks to me awkward and ugly. The original code was much smoother and more easily readable!
The question is not why it happens. I know that it happens because that exception occurs as the RequestHeaderSpec
is being built.
The URI is created dynamically, depending on the request
input parameter. Some combination of fields of the request
is not able to produce a valid URL and such a request should fail. I cannot think of a better place to create the URI. The most logical place seems the method itself. Even if I delegated the URI creation to an external class, it would be called before the Mono
when the RequestHeaderSpec
is being built.
I finally ended up with an "all-in-the-Mono-stream" solution like this:
public Mono<MyData> getMyData(MyRequest request) {
return Mono.just(webClient)
.map(WebClient::get)
.map(uriBuilder -> this.createUri(uriBuilder, request))
.map(WebClient.RequestHeadersSpec::retrieve)
.map(responseSpec -> responseSpec.bodyToMono(MyDataDto.class))
.map(myDataDto -> mapToMyData(myDataDto))
;
}
In contrast to my previous solution from the original question, this one is more "flat", it is only one non-indented Mono fluent stream.