javaspring-bootunit-testingspring-webfluxwebtestclient

Spring RequestContextHolder and WebTestClient


I'm using Spring RequestContextHolder in the controller and it works fine. But in the unit test I get java.lang.IllegalStateException using WebTestClient. Here is an example:

package demo.reactive.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import reactor.core.publisher.Mono;

@RestController
public class FooController {

  @GetMapping("/foo")
  public ResponseEntity<Mono<String>> foo() {

    String sessionId = RequestContextHolder.currentRequestAttributes().getSessionId();

    return ResponseEntity.ok(Mono.just(sessionId));
  }
}
package demo.reactive;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.test.web.reactive.server.WebTestClient;
import demo.reactive.controller.FooController;

@WebFluxTest(FooController.class)
class DemoReactiveApplicationTests {

  @Autowired private WebTestClient client;

  @Test
  void contextLoads() {
    client.get().uri("/foo").exchange().expectStatus().isOk();
  }
}

java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): |_ checkpoint ⇢ HTTP GET "/foo" [ExceptionHandlingWebHandler] Stack trace: at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE] at demo.reactive.controller.FooController.foo(FooController.java:15) ~[classes/:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na] at org.springframework.web.reactive.result.method.InvocableHandlerMethod.lambda$invoke$0(InvocableHandlerMethod.java:147) ~[spring-webflux-5.2.5.RELEASE.jar:5.2.5.RELEASE] at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:151) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:48) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:274) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:851) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:173) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2274) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:132) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:162) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2082) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:1956) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onSubscribe(MonoPeekTerminal.java:145) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.Mono.subscribe(Mono.java:4210) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:441) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:211) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:161) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.Mono.subscribe(Mono.java:4210) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:172) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at org.springframework.test.web.reactive.server.HttpHandlerConnector.lambda$doConnect$1(HttpHandlerConnector.java:97) ~[spring-test-5.2.5.RELEASE.jar:5.2.5.RELEASE] at org.springframework.mock.http.client.reactive.MockClientHttpRequest.lambda$null$2(MockClientHttpRequest.java:121) ~[spring-test-5.2.5.RELEASE.jar:5.2.5.RELEASE] at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.Mono.subscribe(Mono.java:4210) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxConcatIterable$ConcatIterableSubscriber.onComplete(FluxConcatIterable.java:146) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.FluxConcatIterable.subscribe(FluxConcatIterable.java:60) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoFromFluxOperator.subscribe(MonoFromFluxOperator.java:72) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at org.springframework.test.web.reactive.server.HttpHandlerConnector.doConnect(HttpHandlerConnector.java:108) ~[spring-test-5.2.5.RELEASE.jar:5.2.5.RELEASE] at org.springframework.test.web.reactive.server.HttpHandlerConnector.lambda$connect$0(HttpHandlerConnector.java:79) ~[spring-test-5.2.5.RELEASE.jar:5.2.5.RELEASE] at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.Mono.subscribe(Mono.java:4210) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.publisher.MonoSubscribeOn$SubscribeOnSubscriber.run(MonoSubscribeOn.java:124) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE] at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na] at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na] at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

How can I get the unit test to work. Or it's not even suppose to use RequestContextHolder with WebFlux?


Solution

  • Spring RequestContextHolder cannot be used with Spring Webflux. RequestContextHolder primary works using java.lang.ThreadLocal. This clearly doesn't fit with Reactor architecture.

    Having said that, We can implement an alternative using reactor's Mono#subscriberContext() operator. The best way to create a reusable solution is to implement spring WebFilter to get hold of request attributes and set it in the reactor context.

    public class WebRequestAttributesContextFilter implements WebFilter {
    
      public static final String WEB_REQUEST_ATTRIBUTES = "WebRequestAttributes";
    
      @Override
      public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
        return webFilterChain.filter(serverWebExchange)
            .subscriberContext(context -> context.put(WEB_REQUEST_ATTRIBUTES, serverWebExchange.getAttributes()));
      }
    }
    

    The attributes (sessionId in your case) can then be read from the reactor context anywhere in the request processing pipeline as follows:

    @GetMapping("/foo")
    public Mono<String> helloWorld() {
      return Mono.subscriberContext().map(context -> (String) context.<Map<String, Object>>get(
              WebRequestAttributesContextFilter.WEB_REQUEST_ATTRIBUTES
          ).get("session"));
    }