Hi, I have a piece of code in a reactive project that looks something like that (similar code for demo purposes):

@Cacheable(value = RedisCacheNames.CONSTANT, cacheManager = "redisCacheManager")
public Mono<String> get(Integer id) {
      return Mono.just("result")
          .doOnSuccess(s -> throwException())
  }

  private void throwException() {
    throw new RuntimeException();
  }

I have a specific exception, but it basically extends from RuntimeException.

I've been debugging a little, and it seems that CacheAspectSupport had a piece of code added in commit 8974da2a that changed ReactiveCachingHandler from this:

return adapter.fromPublisher(Mono.fromFuture(cachedFuture)
                            .switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts)))
                            .flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts)));

to this:

return adapter.fromPublisher(Mono.fromFuture(cachedFuture)
                            .switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts)))
                            .flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts))
                            .onErrorResume(RuntimeException.class, ex -> {
                                try {
                                    getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
                                    return evaluate(null, invoker, method, contexts);
                                }
                                catch (RuntimeException exception) {
                                    return Mono.error(exception);
                                }
                            }));

The thing is, first evaluate call before the method execution sets the contexts.processed to true, and after my method throws the runtime exception, is caught by this onErrorResume, which calls evaluate with cacheHit set to null, and

if (contexts.processed) {
    return cacheHit;
}

returns null.

Is this an actual issue or I am missing something?

As of now I've done a couple of extra tests, and it seems that I cannot throw a RuntimeException. I have two CacheManagers configured in my Spring Boot project, one for Caffeine and the other one for Redis. This only happens when using the RedisCacheManager as seen above.

I think this might be because CaffeineCache implementation of Cache returns null when the element is not present whereas RedisCache implementation returns a cachedFuture.

Thanks in advance for any help provided!

Comment From: simonbasle

If your exception is a subtype of RuntimeException, and you have an error handler in place, perhaps you can tune the error handler to rethrow that particular type of exception ?

Comment From: drg2000

Hi @simonbasle ,

I think I don't understand your proposal. In the project we have a controller advice that catches our business runtime exceptions. The thing is, that if I throw SpecificException my advice expects that specific class type, but since @Cacheable throws a null pointer it is handled by our generic handler that catches Exception.class. For the client, the message should look like: "This is our business exception" and not like "The nextFactory returned a null Publisher".

What kind of error handling are you proposing in this case?

Thanks for your time

Comment From: simonbasle

@drg2000 I see, the NPE's message is important context. I'll investigate further, but if you can provide a unit test or simple reproducer it would help tremendously.

Comment From: drg2000

Hi @simonbasle and sorry for the late reply, been kind of busy lately.

I've created a small spring project which replicates the issue. You just can launch the Application test or run the app and NullPointerException should be displayed in the console. Any more information I can provide please let me know, the repo url is here: https://github.com/drg2000/bug-demo.

Thanks for your time

Comment From: simonbasle

Thanks for the reproducing project, it helped me verify that my tentative fix was working for your case 👍

Comment From: drg2000

Hi @simonbasle, thanks for your help!

It would be helpful If you could post a comment here once the fix is done, just out of curiosity to see which was the solution.

Thanks for your time.