reactor / reactor-addons

Additional optional modules for the Reactor project

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

RxJava3Adaptor.monoToSingle causes inconsistent behavior in IntegrationTest and real-world test

kanesee opened this issue · comments

I'm unable to get the Principal from ServerWebExchange when it's wrapped with RxJava3Adaptor.monoToSingle

Expected Behavior

Returns Principal's name in example below

Actual Behavior

Returns "no principal" in example below

Steps to Reproduce

Here's a simple example

import reactor.adapter.rxjava.RxJava3Adapter;
import reactor.core.publisher.Mono;
import io.reactivex.rxjava3.core.Single;

@RestController
@RequestMapping("/test")
public class MyController {

  @GetMapping("/mono")
  public Mono<String> getMono(ServerWebExchange exchange) {
    return exchange.getPrincipal()
      .map(principal -> principal.getName())
      .switchIfEmpty(Mono.just("no principal"));
  }

  @GetMapping("/single")
  public Single<String> getSingle(ServerWebExchange exchange) {
    return RxJava3Adapter.monoToSingle(exchange.getPrincipal()
      .map(principal -> principal.getName())
      .switchIfEmpty(Mono.just("no principal")));
  }

}

Hitting /test/mono returns the Principal name every time.
Hitting 'test/single` using curl returns "no principal". However, in IntegrationTest, it returns the Principal name.

Here's sample curl command (that fails every time, ie, returns "no principal")

curl --request GET \
--url http://localhost:8080/test/single \
--header 'authorization: Bearer XXXX' \

Here's example Integration test (that succeeds, ie, doesn't return "no principal")

@WithMockUser
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureWebTestClient
public class IntegrationTest {

  @Autowired
  private WebTestClient webClient;

  @Test
  void testPrincipal() {
    final EntityExchangeResult<byte[]> getResult =
        webClient
            .get()
            .uri("/test/single")
            .exchange()
            .expectStatus()
            .isOk()
            .expectBody()
            .returnResult();

    final String response = new String(getResult.getResponseBody());
    assertNotEquals(response, "no principal");
  }

Possible Solution

Your Environment

  • io.projectreactor:reactor-core:3.4.5
  • io.projectreactor.netty:reactor-netty-http
  • JVM version (java -version): OpenJDK Runtime Environment (build 15.0.1+9)
  • OS and version (eg uname -a): Darwin MacBook-Pro.local 20.3.0 Darwin Kernel Version 20.3.0: Thu Jan 21 00:07:06 PST 2021; root:xnu-7195.81.3~1/RELEASE_X86_64 x86_64

Please disregard the integration test results I mentioned. It was always passing because of that special @WithMockUser. If I remove it and send an authorization header, it fails on the /test/single endpoint same as curl.

Attaching runnable code to show the issue
RxBridgeBug.zip

Not sure if this is related, but when I was tracing through the code, I found this method in ReactiveAdapterRegistry.

Seems if reactor is present, then the reactiveadapter is never added. Not sure if that is affecting me.

I assumed it's permitted to have both RxJava and Reactor in the same project otherwise there wouldn't exist a RxJava3Adapter

I traced through the code and one thing I noticed is that the Reactor Context seems to be used. In the Mono case, it holds a SecurityContext which provides the Authenticated Principal. In the Single case, the Reactor Context is empty.

I suspect the bridge is not copying the Reactor Context's state because RxJava has no concept of a Context.

Yes, unfortunately RxJava has no concept of a Context, so introducing an RxJava subchain breaks the access to Contextual data, which includes SecurityContext... One way around that is to explicitly wrap RxJava publishers in a Reactor flatMap or deferContextual Flux/Mono.