Spring webclient is not logging error response and performing consumer action on receiving error

53
January 14, 2022, at 08:00 AM

I have a spring boot service where APIs are exposed via RouterFunction. Once the API request is received , certain validations are triggered. One of the validation, call another API via webclient to validate if received value exist or not. If value do not exist , it has to log the error message and add the error message in array list.

However, below implementation is neither logging the error nor success message nor able to add the error message in array list.

I also tried to use block() but that gives

'IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-3'

If I directly expose one API via RouterFunction just to call that lookup API, everything is working. This shows that making call via validation layer is causing issue, perhaps some issue with synchronous and reactive way of making call.

I am unable to understand the issue with my implementation.Please guide what I am doing wrong.

Validation Class which is triggering external validation

@Slf4j
@Component
@RequiredArgsConstructor
public class EmployeeValidation{
    
    private final IdentityApiClient identityApiClient;
    
    public List<ErrorDetail> validate(Optional<EmployeeRequestDto> employeeRequest) {
        var errors = new ArrayList<ErrorDetail>();
        employeeRequest
                .ifPresent(employee -> {
                    //some other validations
                    if (errors.isEmpty()) {
                        validateIfIdentityExist(employee.getSecuredEmployeeDetail(), errors::add);
                    }
                });
        return errors;
    }
    
    private void validateIfIdentityExist(SecuredEmployeeDetailDto securedEmployeeDetailDto, Consumer<ErrorDetail> errorDetailConsumer) {
        Optional.ofNullable(securedEmployeeDetailDto)
                .map(SecuredEmployeeDetailDto::getIdentity)
                .ifPresent(identityLocal -> {
                    log.info("Going to retrieve identity [{}] detail", identityLocal);
                    identityApiClient.retrieveIdentityDetail(identityLocal)
                            .doOnError(e -> errorDetailConsumer.accept(new ErrorDetail(REQUEST_INVALID_PARAM, e.getMessage())));
                });
    }
}

Webclient which is calling another API to validate value

@Slf4j
@Component
@RequiredArgsConstructor
public class IdentityApiClient {
    private final WebClient identityWebClient;
    private final IdentityProperties identityProperties;
    
    public Mono<IdentityDetail> retrieveIdentityDetail(String identity) {
        log.info("Going to retrieve identity [{}] detail", identity);
        return identityWebClient
                .get()
                .uri(identityProperties.getLookupPath(), Map.of("identity", identity))
                .retrieve()
                .onStatus(httpStatus -> httpStatus.equals(UNAUTHORIZED),
                        clientResponse -> clientResponse.bodyToMono(String.class)
                                .flatMap(identityLookUpErrorResponse -> Mono.error(new IdentityLookUpException(UNAUTHORIZED.value(), IdentityLookUpErrorResponse.builder()
                                        .error(identityLookUpErrorResponse)
                                        .message("Unauthorized Access")
                                        .status(UNAUTHORIZED.value())
                                        .build()))))
                .onStatus(HttpStatus::is4xxClientError,
                        clientResponse -> clientResponse.bodyToMono(IdentityLookUpErrorResponse.class)
                                .switchIfEmpty(Mono.just(IdentityLookUpErrorResponse.builder()
                                        .error("Received Empty Response Body")
                                        .message("Unknown Identity")
                                        .status(NOT_FOUND.value())
                                        .build()))
                                .flatMap(identityLookUpErrorResponse -> Mono.error(new IdentityLookUpException(identityLookUpErrorResponse.getStatus(), identityLookUpErrorResponse))))
                .onStatus(HttpStatus::is5xxServerError,
                        clientResponse -> clientResponse.bodyToMono(IdentityLookUpErrorResponse.class)
                                .switchIfEmpty(Mono.just(IdentityLookUpErrorResponse.builder()
                                        .error("Received Empty Response Body")
                                        .message(INTERNAL_SERVER_ERROR.getReasonPhrase())
                                        .status(INTERNAL_SERVER_ERROR.value())
                                        .build()))
                                .flatMap(identityLookUpErrorResponse -> Mono.error(new IdentityLookUpException(identityLookUpErrorResponse.getStatus(), identityLookUpErrorResponse))))
                .bodyToMono(IdentityDetail.class)
                .doOnNext(response -> log.info("Identity [{}] response received", response)) // not getting logged when called via validator class
                .doOnError(e -> log.error("Identity [{}] error response received", identity, e));// not getting logged when called via validator class
    }   
}
Answer 1

The method identityApiClient.retrieveIdentityDetail returns a Mono which is not being subscribed to by anyone. This code in isolation does not do anything.

identityApiClient.retrieveIdentityDetail(identityLocal)
                            .doOnError(e -> errorDetailConsumer.accept(new ErrorDetail(REQUEST_INVALID_PARAM, e.getMessage())));

The mantra for reactive programming is

"Nothing happens until you subscribe"

For the majority of use cases, the underlying framework (in this case spring) will subscribe to the Mono for you, as long as you return it from your RouterFunction.

I am not fully familiar with your requirement but one possible solution would be to refactor your validator to make it reactive and add it into your reactive stream.

READ ALSO
How to make a website that makes websites [closed]

How to make a website that makes websites [closed]

Want to improve this question? Update the question so it focuses on one problem only by editing this post

51
Android Room database - Delete rows after row limit?

Android Room database - Delete rows after row limit?

I'm trying to delete any inactive rows after a limit of 1000 rows, and I've tried this:

64
Unload javascript file when modal dialog is closed

Unload javascript file when modal dialog is closed

I have a modal dialog that has been styled to show along the right hand side of the customers browserDepending on the icon they click in a nav bar (here is an example)

30
Node server :remote-addr displayed local IP (192.X.X.X) when accessed from python-requests

Node server :remote-addr displayed local IP (192.X.X.X) when accessed from python-requests

I have an express server that uses nginx and monitors the X-Forwarded-For header

42