WebFilter in WebFlux application
Asked Answered
H

4

5

I have a Spring Boot WebFlux application using Spring Boot 2.0.0.M5/2.0.0.BUILD-SNAPSHOT. I have a requirement to add trace-ids to all logs.

In order to get this to work in a WebFlux application, I tried using the WebFilter approach described here and here

@Component
public class TraceIdFilter implements WebFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    return chain.filter(exchange).subscriberContext((Context context) ->
        context.put(AuditContext.class, getAuditContext(exchange.getRequest().getHeaders()))
    );
}

My controller

@GetMapping(value = "/some_mapping")
public Mono<ResponseEntity<WrappedResponse>> getResource(@PathVariable("resourceId") String id) {
    Mono.subscriberContext().flatMap(context -> {
        AuditContext auditContext = context.get(AuditContext.class);
        ...
    });

The problem I have is that the filter method never gets executed, and the context is not set. I have confirmed that the Webfilter is loaded on startup. Is there anything else needed to get the filter to work?

Hollyanne answered 24/10, 2017 at 22:11 Comment(0)
H
4

It turns out the reason for this not working was because I had dependencies on both spring-boot-starter-web and spring-boot-starter-webflux.

compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-webflux")

The reason I added spring-boot-starter-web as well, is because I was getting the following exception when I removed the dependency

Caused by: java.io.FileNotFoundException: class path resource [org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.class] cannot be opened because it does not exist
at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:177) ~[spring-core-5.0.0.RELEASE.jar:5.0.0.RELEASE]
at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:51) ~[spring-core-5.0.0.RELEASE.jar:5.0.0.RELEASE]
at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:99) ~[spring-core-5.0.0.RELEASE.jar:5.0.0.RELEASE]

I have found that the reason I got this error, was because I have a custom boot starter with a configuration class in EnableAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.x.y.z.MyConfiguration

This configuration class also got picked up during component-scan which seemed to cause a few issues. After removing the dependency on spring-boot-starter-web, the WebFilter started working.

Hollyanne answered 25/10, 2017 at 4:10 Comment(1)
I have the same problem and it was solved by removing dependency on spring-boot-starter-web. Is there any way you can use both the dependencies in the same springboot application? I need the web for non reactive endpoints.Crystallization
L
6

I had a ton of issues figuring this out so hopefully it helps someone. My use case was to validate a signature on the request. This required me to parse the request body for PUT/POST's. The other major use case I see is logging so the below will be helpful too.

MiddlewareAuthenticator.java

@Component
public class MiddlewareAuthenticator implements WebFilter { 

    @Autowired private RequestValidationService requestValidationService;

@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain chain) {
  return HEALTH_ENDPOINTS
      .matches(serverWebExchange)
      .flatMap(
          matches -> {
            if (matches.isMatch()) {
              return chain.filter(serverWebExchange);
            } else {
              return requestValidationService
                  .validate(serverWebExchange, 
                       new BiPredicate<ServerWebExchange, String> { 
                         @Override
                         public boolean test(ServerWebExchange e, String body) {
                             /** application logic can go here. few points:
                              1. I used a BiPredicate because I just need a true or false if the request should be passed to the controller. 
                              2. If you want todo other mutations you could swap the predicate to a normal function and return a mutated ServerWebExchange. 
                              3. I pass body separately here to ensure safety of accessing the request body and not having to rewrap the ServerWebExchange. A side affect of this though is any mutations to the String body do not affect downstream.
                              **/
                              return true;
                            }

                      })
                 .flatMap((ServerWebExchange r) -> chain.filter(r));
            }});
}

RequestValidationService.java

@Service
public class RequestValidationService {
private DataBuffer stringBuffer(String value) {
  byte[] bytes = value.getBytes(StandardCharsets.UTF_8);

  NettyDataBufferFactory nettyDataBufferFactory =
      new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
  DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
  buffer.write(bytes);
  return buffer;
}

private String bodyToString(InputStream bodyBytes) {
  byte[] currArr = null;
  try {
    currArr = bodyBytes.readAllBytes();
    bodyBytes.read(currArr);
  } catch (IOException ioe) {
    throw new RuntimeException("could not parse body");
  }

  if (currArr.length == 0) {
    return null;
  }

  return new String(currArr, StandardCharsets.UTF_8);
}

private ServerHttpRequestDecorator requestWrapper(ServerHttpRequest request, String bodyStr) {
  URI uri = request.getURI();
  ServerHttpRequest newRequest = request.mutate().uri(uri).build();
  final DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
  Flux<DataBuffer> newBodyFlux = Flux.just(bodyDataBuffer);
  ServerHttpRequestDecorator requestDecorator =
      new ServerHttpRequestDecorator(newRequest) {
        @Override
        public Flux<DataBuffer> getBody() {
          return newBodyFlux;
        }
      };

  return requestDecorator;
}

private InputStream newInputStream() {
  return new InputStream() {
    public int read() {
      return -1;
    }
  };
}

private InputStream processRequestBody(InputStream s, DataBuffer d) {
  SequenceInputStream seq = new SequenceInputStream(s, d.asInputStream());
  return seq;
}

private Mono<ServerWebExchange> processInputStream(
    InputStream aggregatedBodyBytes,
    ServerWebExchange exchange,
    BiPredicate<ServerHttpRequest, String> predicate) {

  ServerHttpRequest request = exchange.getRequest();
  HttpHeaders headers = request.getHeaders();

  String bodyStr = bodyToString(aggregatedBodyBytes);

  ServerWebExchange mutatedExchange = exchange;

  // if the body exists on the request we need to mutate the ServerWebExchange to not
  // reparse the body because DataBuffers can only be read once;
  if (bodyStr != null) {
    mutatedExchange = exchange.mutate().request(requestWrapper(request, bodyStr)).build();
  }

  ServerHttpRequest mutatedRequest = mutatedExchange.getRequest();

  if (predicate.test(mutatedRequest, bodyStr)) {
    return Mono.just(mutatedExchange);
  }

  return Mono.error(new RuntimeException("invalid signature"));
}

/*
 * Because the DataBuffer is in a Flux we must reduce it to a Mono type via Flux.reduce
 * This covers large payloads or requests bodies that get sent in multiple byte chunks
 * and need to be concatentated.
 *
 * 1. The reduce is initialized with a newInputStream
 * 2. processRequestBody is called on each step of the Flux where a step is a body byte
 *    chunk. The method processRequestBody casts the Inbound DataBuffer to a InputStream
 *    and concats the new InputStream with the existing one
 * 3. Once the Flux is complete flatMap is executed with the resulting InputStream which is
 *    passed with the ServerWebExchange to processInputStream which will do the request validation
 */
public Mono<ServerWebExchange> validate(
    ServerWebExchange exchange, BiPredicate<ServerHttpRequest, String> p) {
  Flux<DataBuffer> body = exchange.getRequest().getBody();

  return body.reduce(newInputStream(), this::processRequestBody)
      .flatMap((InputStream b) -> processInputStream(b, exchange, p));
}

}

BiPredicate docs: https://docs.oracle.com/javase/8/docs/api/java/util/function/BiPredicate.html

Lead answered 28/8, 2019 at 7:51 Comment(4)
thanks so much, Jordan! but how is postParams used here? can't figure out.. I'd like to be able to somehow transfer this body down to the actual @Controller but can't think of how to go about it yet :(Fr
I actually implemented all I wanted; mutate() and ServerHttp*Decorator are key!Fr
@Fr I updated the code, since publishing I abstracted the mechanics of the WebFilter and ServerWebExchange from application logic through the BiPredicate interface. Hopefully this is more clear. Per my comments you can modify the code to use a java Function instead BiPredicate if you don't want a simple true/false for request authentication. Glad you found a solutionLead
That is brilliant!Project
H
4

It turns out the reason for this not working was because I had dependencies on both spring-boot-starter-web and spring-boot-starter-webflux.

compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-webflux")

The reason I added spring-boot-starter-web as well, is because I was getting the following exception when I removed the dependency

Caused by: java.io.FileNotFoundException: class path resource [org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.class] cannot be opened because it does not exist
at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:177) ~[spring-core-5.0.0.RELEASE.jar:5.0.0.RELEASE]
at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:51) ~[spring-core-5.0.0.RELEASE.jar:5.0.0.RELEASE]
at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:99) ~[spring-core-5.0.0.RELEASE.jar:5.0.0.RELEASE]

I have found that the reason I got this error, was because I have a custom boot starter with a configuration class in EnableAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.x.y.z.MyConfiguration

This configuration class also got picked up during component-scan which seemed to cause a few issues. After removing the dependency on spring-boot-starter-web, the WebFilter started working.

Hollyanne answered 25/10, 2017 at 4:10 Comment(1)
I have the same problem and it was solved by removing dependency on spring-boot-starter-web. Is there any way you can use both the dependencies in the same springboot application? I need the web for non reactive endpoints.Crystallization
H
1

I had similar issue where I had dependency on both

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
</dependency>

and

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

We have to pub exclusion in spring-boot-starter-web for tomcat and netty.

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-reactor-netty</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
Hukill answered 25/6, 2021 at 15:40 Comment(0)
M
0

In my case, I'm not importing the artifactId "spring-boot-starter-webflux" but instead "spring-boot-webflux". What this means is that you need to have a spring bean configured using @Configuration and then place @EnableWebflux as shown in the documentation

Moslemism answered 4/3 at 10:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.