Request
For reactive WebClient
, introduce some possibility to build the URI in the Mono
context when it is subscribed, and not when the RequestHeaderSpec
is being built. It is unpleasant that the possible exceptions thrown there are thrown in the main thread and not in the Mono
context.
Motivation
Code
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.
Tests
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 exceptions in the context of the Mono
subscription.
Problem statement
I can wrap the code so all exceptions are thrown when the Mono
is subscribed, but the solution is awkward and ugly. The original code was much smoother and more easily readable!
public Mono<MyData> getMyData(MyRequest request) {
return Mono.just(request)
.flatMap(req -> webClient
.get()
.uri(uriBuilder -> this.createUri(uriBuilder, req))
.retrieve()
.bodyToMono(MyDataDto.class))
.map(myDataDto -> mapToMyData(myDataDto))
;
}
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.
Suggested Solution
uriDeferred()
A possible solution might be to introduce a new set of methods to build URI, which will just pass the lambdas called later when the Mono
is subscribed. A possible naming uriDeferred()
:)
public Mono<MyData> getMyData(MyRequest request) {
return webClient
.get()
.uriDeferred(uriBuilder -> this.createUri(uriBuilder, request))
.retrieve()
.bodyToMono(MyDataDto.class)
.map(myDataDto -> mapToMyData(myDataDto))
;
}
RequestHeaderSpecMono
Another solution might be something like RequestHeaderSpecMono extends Mono
, with added extra methods similar to those in WebClient
.
public Mono<MyData> getMyData(MyRequest request) {
return webClient
.createRequestHeaderSpecMono() // RequestHeaderSpecMono
// All the exceptions thrown from now on are in the Mono context
.get() // RequestHeaderSpecMono
.uri(uriBuilder -> this.createUri(uriBuilder, request)) // RequestHeaderSpecMono
.retrieve() // RequestHeaderSpecMono
.bodyToMono(MyDataDto.class) // Mono
.map(myDataDto -> mapToMyData(myDataDto))
;
}
Comment From: bclozel
I'm not sure we need to add such a variant here. Have you considered wrapping this call with Mono.defer()?
Comment From: honza-zidek
@bclozel I managed to wrap it, but it is not elegant. And in my opinion, the way the RequestHeaderSpec
is constructed goes against the philosophy of the reactive streams.
Look at this:
return webClient
.get()
.uri(uriBuilder -> this.createUri(uriBuilder, request))
.retrieve()
.bodyToMono(MyDataDto.class)
.map(myDataDto -> mapToMyData(myDataDto))
;
This looks just like a common standard reactive stream. One would fully expect that all the lambdas, including the one called in the uri()
method, are executed in the context of the Mono
upon subscription. However, they are not :(
So it would be more consistent if I may rely that all the "asynchronous code" (e.g. all the lambdas) are behaving according to the same logic.
And I must admit that the first time my intuitive test (see above) failed, I was confused. Then I realized the reason behind it, but it still is counter-intuitive at the first glance.
Comment From: bclozel
And in my opinion, the way the RequestHeaderSpec is constructed goes against the philosophy of the reactive streams.
I don't think it does. The action described by the publisher should only happen when a subscription starts, but here this is about the building of the publisher which does not happen asynchronously.
The same could be argued for all other inputs like HTTP method, attributes and so on. The defer operator is designed for such cases.