Context-Propagation does not work in @SpringBootTest

435 views Asked by At

my Spring-WebMvc application which uses WebClient (reactor) loses the context in a @SpringBootTest.

I have a @SpringBootApplication which offers a @RestController endpoint to the caller. When the caller requests this endpoint, my application makes a Rest call to a third-party Rest-Endpoint by using a WebClient.

The webclient has two filters configured:

  1. ExchangeFilterFunction.ofRequestProcessor
  2. ExchangeFilterFunction.ofResponseProcessor

The filters need to write information (request and response body) to a ThreadLocal. After the requests to the third-party api is done, i want to read the information of the thread-local into a custom DataStructure and return it to the caller of my api.

This works when i run the application but not in an integration test. Here are some details about the application:

  1. I use Hooks.enableAutomaticContextPropagation()

  2. I create a ThreadLocalAccessor

ContextRegistry.getInstance().registerThreadLocalAccessor("THREAD_LOCAL",
    InformationBasket.THREAD_LOCAL::get,
    InformationBasket.THREAD_LOCAL::set,
    InformationBasket.THREAD_LOCAL::remove
);
  1. The ThreadLocal is "wrapped" in a simple class:
public class InformationBasket {
    public static final ThreadLocal<Map<String, String>> THREAD_LOCAL = new ThreadLocal<>();
}
  1. I have the context-propagation dependency on the classpath:
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>context-propagation</artifactId>
  <version>1.0.4</version>
  <scope>compile</scope>
</dependency>
  1. The filters of the webclient look like this:

    private ExchangeFilterFunction getRequestFilter() {
        return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
            final ThreadLocal<Map<String, String>> threadLocal = InformationBasket.THREAD_LOCAL;
            final Map<String, String> arg = threadLocal.get();
            log.info("--> ThreadLocal in RequestFilter: {}", threadLocal);
            log.info("--> Map in ThreadLocal in RequestFilter: {}", arg);
            arg.put("Request", "some-request-data");
            return Mono.just(clientRequest);
        });
    }
    
    private ExchangeFilterFunction getResponseFilter() {
        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
            final ThreadLocal<Map<String, String>> threadLocal = InformationBasket.THREAD_LOCAL;
            final Map<String, String> arg = threadLocal.get();
            log.info("<-- ThreadLocal in ResponseFilter: {}", threadLocal);
            log.info("<-- Map in ThreadLocal in ResponseFilter: {}", arg);
            arg.put("Response", "some-response-data");
            return Mono.just(clientResponse);
        });
    }
  1. The RestController looks like this:

    @GetMapping("/simple")
    public String simpleEndpoint() {
    
        InformationBasket.THREAD_LOCAL.set(new HashMap<>());
        final ThreadLocal<Map<String, String>> threadLocal = InformationBasket.THREAD_LOCAL;
        log.info("ThreadLocal in Endpoint: {}", threadLocal);
    
        final URI uri = UriComponentsBuilder.fromHttpUrl(url).path("3rd/").path("order").build().toUri();
    
        final String response = webClient.get()
                .uri(uri)
                .retrieve()
                .toEntity(String.class)
                .mapNotNull(HttpEntity::getBody)
                .block();
    
        // We expect that the Map of the ThreadLocal contains data from both filters (request and response)
        InformationBasket.THREAD_LOCAL.get().forEach((key, value) -> {
            log.info("Key: {} Value: {}", key, value);
        });
    
        return response;
    }
  1. The integration test looks like this:

@SpringBootTest(classes = ReactorTestingApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SimpleControllerTest {

    @LocalServerPort
    int port;
    
    @Autowired
    private WebTestClient client;
    
    @Test
    void simpleEndpoint_success() throws Exception {
        client.get().uri("/simple")
                .exchange()
                .expectStatus()
                .isOk();
    }

}

The logs in the test look like this:


2023-09-11T12:30:53.265+02:00  INFO 16745 --- \[o-auto-1-exec-1\] c.e.reactortesting.SimpleController      : ThreadLocal in Endpoint: java.lang.ThreadLocal@49f16008
2023-09-11T12:30:53.271+02:00  INFO 16745 --- \[o-auto-1-exec-1\] c.e.reactortesting.SimpleController      : --\> ThreadLocal in RequestFilter: java.lang.ThreadLocal@49f16008
2023-09-11T12:30:53.272+02:00  INFO 16745 --- \[o-auto-1-exec-1\] c.e.reactortesting.SimpleController      : --\> Map in ThreadLocal in RequestFilter: {}

------

\--\> Exception: java.lang.NullPointerException: Cannot invoke "java.util.Map.put(Object, Object)" because "arg" is null

The logs in the running application look like this:


2023-09-11T12:33:42.083+02:00  INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController      : ThreadLocal in Endpoint: java.lang.ThreadLocal@771db298
2023-09-11T12:33:42.092+02:00  INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController      : --\> ThreadLocal in RequestFilter: java.lang.ThreadLocal@771db298
2023-09-11T12:33:42.092+02:00  INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController      : --\> Map in ThreadLocal in RequestFilter: {}
2023-09-11T12:33:42.409+02:00  INFO 16902 --- \[ctor-http-nio-2\] c.e.reactortesting.SimpleController      : \<-- ThreadLocal in ResponseFilter: java.lang.ThreadLocal@771db298
2023-09-11T12:33:42.409+02:00  INFO 16902 --- \[ctor-http-nio-2\] c.e.reactortesting.SimpleController      : \<-- Map in ThreadLocal in ResponseFilter: {Request=some-request-data}
2023-09-11T12:33:42.418+02:00  INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController      : Key: Response Value: some-response-data
2023-09-11T12:33:42.418+02:00  INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController      : Key: Request Value: some-request-data

As you can see, the application behaves different in the IntegrationTest. What is causing this different behaviour?

Thank you in advance. Best,J

  • Tried to use MockMvc, WebTestClient
  • Tried to use different versions of the context-propagation library
  • Tried Mono.deferContextual() in the filter-functions
0

There are 0 answers