Search code examples
javaspring-webfluxspring-security-oauth2

random NullPointerException / onErrorDropped using webClient, due to request.getSession() being null


I have a Spring Boot (2.5) application in which I need to make a REST call to a remote system (a Solr instance where I store a denormalized view), in which I can either create or update records.

I don't really care about the response I get (and sometimes the remote system is slow to respond), so I am making an async call like this in createIndexForTicket / updateIndexForTicket :

public MyService(WebClient webClient, String solrUpdateUrl) {
    this.webClient = webClient;
    this.solrUpdateUrl = solrUpdateUrl;
}

  public void createIndexForTicket(TicketIndex ticketIndex) {
    // build the request
    var createRequest = webClient.post()
        .uri(solrUpdateUrl);

    triggerRequest(createRequest, ticketIndex,"creation");

    log.info("payload sent, creating index for ticket {} : {}",ticketIndex.getUserFriendlyTicketId(),ticketIndex);
  }

  
  public void updateIndexForTicket(TicketIndex ticketIndex) {
    // build the request
    var updateRequest = webClient.put()
        .uri(solrUpdateUrl + "/" + ticketIndex.getInternalTicketId());

    triggerRequest(updateRequest, ticketIndex,"update");

    log.info("payload sent, updating index for ticket {} : {}",ticketIndex.getUserFriendlyTicketId(),ticketIndex);
  }

  private static void triggerRequest(RequestBodySpec requestToSolr,
                                                                 TicketIndex ticketIndex,
                                                                  String action) {

    requestToSolr.bodyValue(ticketIndex)
        .retrieve()
        .onStatus(HttpStatus::is2xxSuccessful,
                  resp -> logSuccess(ticketIndex,action))
        .bodyToMono(String.class)
        .doOnError(t ->
            log.error("problem while performing a "+action+", "
                + "calling Solr for ticket "+ticketIndex.getUserFriendlyTicketId(),t))
        .subscribe();
  }

it works fine, most of the times. But I noticed that I sometimes get an Operator called default onErrorDropped error, with below stacktrace :

reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.NullPointerException
Caused by: java.lang.NullPointerException: null
    at org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository.saveAuthorizedClient(HttpSessionOAuth2AuthorizedClientRepository.java:63)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ Request to PUT https://myRemoteSolrSystem/services/v2/tickets/dGlja2V0aW5nLXNlcnZpY2UxNDEzNzM1 [DefaultWebClient]
Stack trace:
        at org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository.saveAuthorizedClient(HttpSessionOAuth2AuthorizedClientRepository.java:63)
        at org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository.saveAuthorizedClient(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java:92)
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.lambda$new$0(DefaultOAuth2AuthorizedClientManager.java:126)
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:184)
        at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$24(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:552)
        at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:86)
        at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:227)
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68)
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:834)

Looking in source code, I find this leads to spring-security-oauth2-client 5.5.1, in HttpSessionOAuth2AuthorizedClientRepository.saveAuthorizedClient

@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
        HttpServletRequest request, HttpServletResponse response) {
    Assert.notNull(authorizedClient, "authorizedClient cannot be null");
    Assert.notNull(request, "request cannot be null");
    Assert.notNull(response, "response cannot be null");
    Map<String, OAuth2AuthorizedClient> authorizedClients = this.getAuthorizedClients(request);
    authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient);
    request.getSession().setAttribute(this.sessionAttributeName, authorizedClients);
}

l.63, where the exception happens is the last one :

request.getSession().setAttribute(this.sessionAttributeName, authorizedClients);

So it looks like request.getSession() returns null... but I have no idea why, and I am not able to find a pattern. Sometimes I fire 2 consecutive calls from the same thread, one is successful while the other is not.. sometimes both fail, and sometimes both succeed. Some other time, I trigger only one call and it fails, while another thread does something similar more or less at the same time, and it works.

The webClient that gets injected is built like that :

@Bean
@Primary
WebClient servletWebClient(ClientRegistrationRepository clientRegistrations,
                         OAuth2AuthorizedClientRepository authorizedClients) {

var oauth = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);

oauth.setDefaultClientRegistrationId("keycloak");

return WebClient.builder()
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .apply(oauth.oauth2Configuration())
    .build();
}

Any hint on what I am not doing correctly, or on what I could try to understand better what is going on ?

Thanks


Solution

  • Here's the workaround that seems to work :

    declare a threadExecutor :

     private final ExecutorService solrRequestExecutor = Executors.newSingleThreadExecutor();
    

    then, make the async call through it :

    private void triggerRequest(RequestBodySpec requestToSolr,
                                                                 TicketIndex ticketIndex,
                                                                  String action) {
    
    // performing calls to Solr asynchronously
    solrRequestExecutor.submit(
        () ->
    requestToSolr.bodyValue(ticketIndex)
        .retrieve()
        .onStatus(HttpStatus::is2xxSuccessful,
                  resp -> logSuccess(ticketIndex,action))
        .bodyToMono(String.class)
        .doOnError(t ->
            log.error("problem while performing a "+action+", "
                + "calling Solr for ticket "+ticketIndex.getUserFriendlyTicketId(),t))
        .block());
    }
    

    Since this doesn't execute the in the same thread anymore, the webClient has to be configured correctly, otherwise we get a servletRequest cannot be null error. see

    test a Spring Boot WebClient outside of a HttpServletRequest context

    I am still not sure why the original code fails randomly though... does it work only if the remote endpoint is also a reactive one ?