javaspringspring-webfluxproject-reactorspring-webclient

How to have all the exceptions thrown in the Mono context when using Spring reactive WebClient?


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 exception in the context of the Mono subscription.

Question

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.


Solution

  • 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.