Hot questions for Spring WebFlux

Top 10 Java Open Source / Spring / Spring WebFlux

Question:

I'm trying to log a request using Spring 5 WebClient. Do you have any idea how could I achieve that?

(I'm using Spring 5 and Spring boot 2)

The code looks like this at the moment:

try {
    return webClient.get().uri(url, urlParams).exchange().flatMap(response -> response.bodyToMono(Test.class))
            .map(test -> xxx.set(test));
} catch (RestClientException e) {
    log.error("Cannot get counter from opus", e);
    throw e;
}

Answer:

You don't necessarily need to roll your own logger, reactor.ipc.netty.channel.ChannelOperationsHandler does it for you. Just configure your logging system for that class to log at DEBUG level:

2017-11-23 12:52:04.562 DEBUG 41449 --- [ctor-http-nio-5] r.i.n.channel.ChannelOperationsHandler   : [id: 0x9183d6da, L:/127.0.0.1:57681 - R:localhost/127.0.0.1:8000] Writing object DefaultFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 0))
GET /api/v1/watch/namespaces/default/events HTTP/1.1
user-agent: ReactorNetty/0.7.1.RELEASE
host: localhost:8000
accept-encoding: gzip
Accept: application/json
content-length: 0

One way to have fewer bugs is to not write code whenever possible.

Nov 2018:

With spring-webflux:5.1.2.RELEASE, the above no longer works. Use the following instead:

logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=DEBUG
...
2018-11-06 20:58:58.181 DEBUG 20300 --- [           main] o.s.w.r.f.client.ExchangeFunctions       : [2026fbff] HTTP GET http://localhost:8080/stocks/search?symbol=AAPL
2018-11-06 20:58:58.451 DEBUG 20300 --- [ctor-http-nio-4] o.s.w.r.f.client.ExchangeFunctions       : [2026fbff] Response 400 BAD_REQUEST

To log headers or form body, set the above to TRACE level; however, that's not enough:

ExchangeStrategies exchangeStrategies = ExchangeStrategies.withDefaults();
exchangeStrategies
    .messageWriters().stream()
    .filter(LoggingCodecSupport.class::isInstance)
    .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true));

client = WebClient.builder()
    .exchangeStrategies(exchangeStrategies)

Mar 2019:

In response to a question in the comment that asked how to log request and response body, I don’t know if Spring has such a logger but WebClient is built on Netty, so enabling debug logging for package reactor.ipc.netty should work, along with this answer.

Question:

I want to have centralised logging for requests and responses in my REST API on Spring WebFlux with Kotlin. So far I've tried this approaches

@Bean
fun apiRouter() = router {
    (accept(MediaType.APPLICATION_JSON) and "/api").nest {
        "/user".nest {
            GET("/", userHandler::listUsers)
            POST("/{userId}", userHandler::updateUser)
        }
    }
}.filter { request, next ->
    logger.info { "Processing request $request with body ${request.bodyToMono<String>()}" }
    next.handle(request).doOnSuccess { logger.info { "Handling with response $it" } }
}

Here request method and path log successfully but the body is Mono, so how should I log it? Should it be the other way around and I have to subscribe on request body Mono and log it in the callback? Another problem is that ServerResponse interface here doesn't have access to the response body. How can I get it here?


Another approach I've tried is using WebFilter

@Bean
fun loggingFilter(): WebFilter =
        WebFilter { exchange, chain ->
            val request = exchange.request
            logger.info { "Processing request method=${request.method} path=${request.path.pathWithinApplication()} params=[${request.queryParams}] body=[${request.body}]"  }

            val result = chain.filter(exchange)

            logger.info { "Handling with response ${exchange.response}" }

            return@WebFilter result
        }

Same problem here: request body is Flux and no response body.

Is there a way to access full request and response for logging from some filters? What don't I understand?


Answer:

This is more or less similar to the situation in Spring MVC.

In Spring MVC, you can use a AbstractRequestLoggingFilter filter and ContentCachingRequestWrapper and/or ContentCachingResponseWrapper. Many tradeoffs here:

  • if you'd like to access servlet request attributes, you need to actually read and parse the request body
  • logging the request body means buffering the request body, which can use a significant amount of memory
  • if you'd like to access the response body, you need to wrap the response and buffer the response body as it's being written, for later retrieval

ContentCaching*Wrapper classes don't exist in WebFlux but you could create similar ones. But keep in mind other points here:

  • buffering data in memory somehow goes against the reactive stack, since we're trying there to be very efficient with the available resources
  • you should not tamper with the actual flow of data and flush more/less often than expected, otherwise you'd risk breaking streaming uses cases
  • at that level, you only have access to DataBuffer instances, which are (roughly) memory-efficient byte arrays. Those belong to buffer pools and are recycled for other exchanges. If those aren't properly retained/released, memory leaks are created (and buffering data for later consumption certainly fits that scenario)
  • again at that level, it's only bytes and you don't have access to any codec to parse the HTTP body. I'd forget about buffering the content if it's not human-readable in the first place

Other answers to your question:

  • yes, the WebFilter is probably the best approach
  • no, you shouldn't subscribe to the request body otherwise you'd consume data that the handler won't be able to read; you can flatMap on the request and buffer data in doOn operators
  • wrapping the response should give you access to the response body as it's being written; don't forget about memory leaks, though

Question:

How to enable CORS in a Spring 5 Webflux Project?

I cannot find any proper documentation.


Answer:

I had success with this custom filter:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;

import reactor.core.publisher.Mono;


@Configuration
public class CorsConfiguration {

  private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN";
  private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
  private static final String ALLOWED_ORIGIN = "*";
  private static final String MAX_AGE = "3600";

  @Bean
  public WebFilter corsFilter() {
    return (ServerWebExchange ctx, WebFilterChain chain) -> {
      ServerHttpRequest request = ctx.getRequest();
      if (CorsUtils.isCorsRequest(request)) {
        ServerHttpResponse response = ctx.getResponse();
        HttpHeaders headers = response.getHeaders();
        headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
        headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
        headers.add("Access-Control-Max-Age", MAX_AGE);
        headers.add("Access-Control-Allow-Headers",ALLOWED_HEADERS);
        if (request.getMethod() == HttpMethod.OPTIONS) {
          response.setStatusCode(HttpStatus.OK);
          return Mono.empty();
        }
      }
      return chain.filter(ctx);
    };
  }

}

and org.springframework.boot:spring-boot-starter-web should not be included as dependency - filter does not work with it.

Question:

spring-boot-starter-webflux (Spring Boot v2.0.0.M2) is already configured like in a spring-boot-starter-web to serve static content in the static folder in resources. But it doesn't forward to index.html. In Spring MVC it is possible to configure like this:

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("forward:/index.html");
}

How to do it in Spring Webflux?


Answer:

Do it in WebFilter:

@Component
public class CustomWebFilter implements WebFilter {
  @Override
  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    if (exchange.getRequest().getURI().getPath().equals("/")) {
        return chain.filter(exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()).build());
    }

    return chain.filter(exchange);
  }
}

Question:

I am currently trying to upload a file from an Angular 4 front-end to a Spring Webflux controller. The controller is able to read the @RequestPart value but throws a 415 UnsupportedMediaTypeStatusException.

UploadController

@PostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE )
public Mono<Void> save(@RequestPart("file")MultipartFile file) {
    log.info("Storing a new file. Recieved by Controller");
    this.storageService.store(file);
    return Mono.empty();
}

The log.info() method does not execute so it seems that the error is being thrown before the method is executed.

Error message

org.springframework.web.server.UnsupportedMediaTypeStatusException: Response status 415 with reason "Content type 'image/png' not supported"
at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.readBody(AbstractMessageReaderArgumentResolver.java:206) ~[spring-webflux-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.readBody(AbstractMessageReaderArgumentResolver.java:124) ~[spring-webflux-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.web.reactive.result.method.annotation.RequestPartMethodArgumentResolver.lambda$resolveArgument$0(RequestPartMethodArgumentResolver.java:99) ~[spring-webflux-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:391) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:633) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onNext(FluxFlattenIterable.java:238) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:87) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxReplay$SizeBoundReplayBuffer.replayFused(FluxReplay.java:865) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxReplay$SizeBoundReplayBuffer.replay(FluxReplay.java:895) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.ReplayProcessor.onNext(ReplayProcessor.java:436) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.MonoProcessor.drainLoop(MonoProcessor.java:504) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.MonoProcessor.onNext(MonoProcessor.java:347) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:115) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:450) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1069) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:142) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:460) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxCreate$BaseSink.complete(FluxCreate.java:404) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:712) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxCreate$BufferAsyncSink.complete(FluxCreate.java:666) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxCreate$SerializedSink.drainLoop(FluxCreate.java:221) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxCreate$SerializedSink.drain(FluxCreate.java:192) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at reactor.core.publisher.FluxCreate$SerializedSink.complete(FluxCreate.java:187) ~[reactor-core-3.1.5.RELEASE.jar:3.1.5.RELEASE]
at org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader$FluxSinkAdapterListener.onAllPartsFinished(SynchronossPartHttpMessageReader.java:215) ~[spring-web-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.synchronoss.cloud.nio.multipart.NioMultipartParser.allPartsRead(NioMultipartParser.java:603) ~[nio-multipart-parser-1.1.0.jar:na]
at org.synchronoss.cloud.nio.multipart.NioMultipartParser.write(NioMultipartParser.java:449) ~[nio-multipart-parser-1.1.0.jar:na]
at org.synchronoss.cloud.nio.multipart.NioMultipartParser.write(NioMultipartParser.java:370) ~[nio-multipart-parser-1.1.0.jar:na]
at org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader$SynchronossPartGenerator.lambda$accept$0(SynchronossPartHttpMessageReader.java:136) ~[spring-web-5.0.4.RELEASE.jar:5.0.4.RELEASE]

The dependency Spring Webflux should use org.synchronoss.cloud.nio.multipart is loaded so I am not fully understanding why the 415 error is being thrown by Spring.

I created a test using the WebClient in Spring

WebTest

    @Test
public void sendValidFileSaveCorrectly() {
    MockMultipartFile file = new MockMultipartFile("foo", "foo.txt",
            MediaType.TEXT_PLAIN_VALUE, "Hello World".getBytes());
    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("file", file);

    webClient.post()
            .uri("/api/file")
            .syncBody(builder.build())
            .exchange()
            .expectStatus().is2xxSuccessful();
}

I got a new 500 error instead using the MockMultipartFile and this message

I/O failure: org.springframework.core.codec.CodecException: Type definition error: [simple type, class java.io.ByteArrayInputStream]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.mock.web.MockMultipartFile["inputStream"])

What I am trying to understand is why Spring is throwing an Unsupported Media Type exception and how I can direct Spring Webflux to ignore whatever is writing that response.

Update

Tried changing the controller to use @RequestParams and @RequestBody and got the same 415 error. The Multipart request is being handled but the Content-Type of the attached file is what is doing the 415.

I went ahead and added a ExceptionHandler to the Controller to try and catch the UnsupportedMediaTypeStatusException. I am not able to pass through the file to the ExceptionHandler though so this did not work. I could Override the default ExceptionHandler for the UnsupportedMediaTypeStatusException but I would prefer to avoid this if possible.

If it is useful I am posting my Angular service that is uploading the file. However because the error occurs even in tests I don't think there is a problem with the Angular.

upload.service.ts

post(file: File, fileName: string) {
const formData = new FormData();
formData.append('file', file, fileName);

let headers = new HttpHeaders();
headers = headers.delete('Content-Type');

this.http
  .post(this.API_URL, formData, { headers: headers, reportProgress: true })
  .subscribe();

}


Answer:

Please use the @RequestPart("file") Mono<FilePart> file or @RequestPart("file") Flux<FilePart> instead of @RequestPart("file") MultipartFile file.

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Mono<Void> save(@RequestPart("file") Mono<FilePart> file) {
        log.info("Storing a new file. Recieved by Controller");
        this.storageService.store(file);
        return Mono.empty();
    }

Question:

I am creating an API for an Angular 5 application. I would like to use JWT for authentication. I would like to use the features that are provided by spring security so I can easily work with roles.

I managed to disable basic authentication. But when using http.authorizeExchange().anyExchange().authenticated(); I still get a login prompt. I would like to just give a 403 instead of the prompt. So overriding the login prompt by a "thing"(Is it a filter?) that checks the Authorization header for the token.

The login I just want to do in a controller that will return a JWT token. But what spring security bean I should use for checking user credentials? I can build my own services and repositories, but I would like to use the features provided by spring security as much as possible.

The short version of this question is just: How can I customize spring security's authentication? What beans do I have to create? Where do I have to put the configuration? (I now have a bean of SecurityWebFilterChain)

The only documentation I could find about authentication in webflux with spring security is this: https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#jc-webflux


Answer:

After a lot of searching and trying I think I have found the solution:

You need a bean of SecurityWebFilterChain that contains all configuration. This is mine:

@Configuration
public class SecurityConfiguration {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private SecurityContextRepository securityContextRepository;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        // Disable default security.
        http.httpBasic().disable();
        http.formLogin().disable();
        http.csrf().disable();
        http.logout().disable();

        // Add custom security.
        http.authenticationManager(this.authenticationManager);
        http.securityContextRepository(this.securityContextRepository);

        // Disable authentication for `/auth/**` routes.
        http.authorizeExchange().pathMatchers("/auth/**").permitAll();
        http.authorizeExchange().anyExchange().authenticated();

        return http.build();
    }
}

I've disabled httpBasic, formLogin, csrf and logout so I could make my custom authentication.

By setting the AuthenticationManager and SecurityContextRepository I overridden the default spring security configuration for checking if a user is authenticated/authorized for a request.

The authentication manager:

@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        // JwtAuthenticationToken is my custom token.
        if (authentication instanceof JwtAuthenticationToken) {
            authentication.setAuthenticated(true);
        }
        return Mono.just(authentication);
    }
}

I am not fully sure where the authentication manager is for, but I think for doing the final authentication, so setting authentication.setAuthenticated(true); when everything is right.

SecurityContextRepository:

@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {

    @Override
    public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
        // Don't know yet where this is for.
        return null;
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
        // JwtAuthenticationToken and GuestAuthenticationToken are custom Authentication tokens.
        Authentication authentication = (/* check if authenticated based on headers in serverWebExchange */) ? 
            new JwtAuthenticationToken(...) :
            new GuestAuthenticationToken();
        return new SecurityContextImpl(authentication);
    }
}

In the load I will check based on the headers in the serverWebExchange if the user is authenticated. I use https://github.com/jwtk/jjwt. I return a different kind of authentication token if the user is authenticated or not.

Question:

I am learning webflux and I would like to know how to serve static content on a MicroService using webflux but I didn´t find information to do it.


Answer:

Try this

RouterFunction router = resources("/**", new ClassPathResource("public/"));

UPDATE: Don't forget to specify a name of the static file in the URL when accessing it from outside, like localhost:8080/index.html

Question:

I have one question regarding Spring WebClient

In my application I need to do many similar API calls, sometimes I need change headers in the calls (Authentication token). So the question arises, what would be better of the two options:

  1. To create one WebClient for all incoming requests to MyService.class, by making it private final field, like code below:

    private final WebClient webClient = WebClient.builder()
            .baseUrl("https://another_host.com/api/get_inf")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .build();
    

Here arises another question: is WebClient thread-safe? (because service is used by many threads)

  1. To create new WebClient for each new request incoming to service class.

I want to provide maximum performance, and to use it in right way, but I don't know how WebClient works inside it, and how it expects to be used.

Thank you.


Answer:

Two key things here about WebClient:

  1. Its HTTP resources (connections, caches, etc) are managed by the underlying library, referenced by the ClientHttpConnector that you can configure on the WebClient
  2. WebClient is immutable

With that in mind, you should try to reuse the same ClientHttpConnector across your application, because this will share the connection pool - this is arguably the most important thing for performance. This means you should try to derive all WebClient instances from the same WebClient.create() call. Spring Boot helps you with that by creating and configuring for you a WebClient.Builder bean that you can inject anywhere in your app.

Because WebClient is immutable it is thread-safe. WebClient is meant to be used in a reactive environment, where nothing is tied to a particular thread (this doesn't mean you cannot use in a traditional Servlet application).

If you'd like to change the way requests are made, there are several ways to achieve that:

configure things in the builder phase
WebClient baseClient = WebClient.create().baseUrl("https://example.org");
configure things on a per-request basis
Mono<ClientResponse> response = baseClient.get().uri("/resource")
                .header("token", "secret").exchange();
create a new client instance out of an existing one
// mutate() will *copy* the builder state and create a new one out of it
WebClient authClient = baseClient.mutate()
                .defaultHeaders(headers -> {headers.add("token", "secret");})
                .build();

Question:

We are using spring framework 5 and spring boot 2.0.0.M6 and we are also using WebClient for reactive programming. We created test methods for our reactive rest endpoints and so I looked up for some example on how to do it. I found this one or this and many others which where all the same. They just autowire a WebTestClient. So I tried the same:

@Log
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class MyControllerTest {

    @Autowired
    private WebTestClient webClient;

    @Test
    public void getItems() throws Exception {
        log.info("Test: '/items/get'");

        Parameters params = new Parameters("#s23lkjslökjh12", "2015-09-20/2015-09-27");

        this.webClient.post().uri("/items/get")
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .contentType(MediaType.APPLICATION_STREAM_JSON)
                .body(BodyInserters.fromPublisher(Mono.just(params), Parameters.class))
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_STREAM_JSON)
                .expectBody(Basket.class);
    }
}

I cannot run this because I get the error:

Could not autowire. No beans of 'WebTestClient' type found.

So it does not seem that there is a auto configuration existing. Do I use the wrong version or what is the matter here?


Answer:

Annotate your MyControllerTest test class with @AutoConfigureWebTestClient annotation. That should solve the issue.