'Convert ClientResponse to ServerResponse

I'm trying to write a sort of proxy between a REST-Server and a calling client application, in order to enforce privacy by adding attributes to the REST call.

I don't want to inspect the response from the server, I just want to pass it through to the client.

I want to do this with spring-webflux, since I hope to save some CPU with the event-driven approach. But I'm completely stuck.

Here is, what I try:

public Mono<ServerResponse> select(ServerRequest request) {
  return request.principal().flatMap((principal) -> {
    return WebClient.create(solrUrl).get().uri(f -> {
              URI u = f.path(request.pathVariable("a")).path("/b/").queryParams(queryModifier.modify(principal, request.pathVariable("collection"), request.queryParams()).block()).build();
              if (debug) {
                log.debug("Calling {}", u);
              }
              return u;
            })
        .exchange()
          .flatMap((ClientResponse mapper) -> {
            BodyBuilder bodyBuilder = ServerResponse.status(mapper.statusCode());
            bodyBuilder.body(BodyInserters.fromDataBuffers(mapper.bodyToFlux(DataBuffer.class)));
            bodyBuilder.headers(c -> mapper.headers().asHttpHeaders().forEach((name, value) -> c.put(name, value)));
              //.body(DefaultserverreBodyInserters.fromPublisher(mapper.bodyToMono(DataBuffer.class), DataBuffer.class)));
              //.body(BodyInserters.fromDataBuffers(mapper.bodyToFlux(DataBuffer.class))));
              //.body(BodyInserters.fromDataBuffers(mapper.body(BodyExtractors.toDataBuffers()))));
              //.body(mapper.bodyToMono(String.class), String.class));
              //.build());
            return bodyBuilder.build();
          });
  });
}

I bind this to my client facing REST API via a RouterFunction:

@Configuration
public class VoiceRouterFunctions {

  @Bean
  public RouterFunction<ServerResponse> route(ClientPropertiesRequestHandler handler) {
    return RouterFunctions.route(RequestPredicates.GET("/v3/{a}/select")
        .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::select);
  }
}

The response is 200 - OK, and nothing in the body. Any ideas?



Solution 1:[1]

The following is the explanation of your problem, I hope it helps you understand why this happens.

The error in your question is the following: the BodyBuilder.body(...) method actually builds and returns a Mono<ServerResponse>, but it does not modify the builder. You can see this if you look at the source code of the builder.

It is easier to understand by looking at your code:

BodyBuilder bodyBuilder = ServerResponse.status(...);  // 1
bodyBuilder.body(...);                                 // 2
bodyBuilder.headers(...);                              // 3
return bodyBuilder.build();                            // 4

This is what happens: on line 1 you create a builder, on line 2 you add a body and build it, but you don't store the result in a variable (oops!), then on line 3 you add headers to the initial builder, which does not have a body because the body(...) function does not modify the builder, and on line 4 you build and return the empty builder. So, in your case line 2 does nothing.

The solution, as you found out by chance in your answer, is to call the body(...) function at the end, it should be last!

The following example would also have worked. Notice the new order of the lines.

BodyBuilder bodyBuilder = ServerResponse.status(...);  // 1
bodyBuilder.headers(...);                              // 3
return bodyBuilder.body(...);                          // 2

Solution 2:[2]

For some reason I can't fathom, this works:

public Mono<ServerResponse> select(ServerRequest request) {
  return request.principal().flatMap((principal) -> {
    return client.get().uri(f -> {
              URI u = f.path(request.pathVariable("collection")).path("/select/").queryParams(queryModifier.modify(principal, request.pathVariable("collection"), request.queryParams()).block()).build();
              if (debug) {
                log.debug("Calling {}", u);
              }
              return u;
            })
        .exchange()
          .flatMap((ClientResponse mapper) -> {
            return ServerResponse.status(mapper.statusCode())
              .headers(c -> mapper.headers().asHttpHeaders().forEach((name, value) -> c.put(name, value)))
              .body(mapper.bodyToFlux(DataBuffer.class), DataBuffer.class);
          });
  });
}

I have tried a dozen or more variations of this, and nothing worked before, but now this... Can anyone explain, why, and why it has to be specifically this? Sorry, but an API that doesn't lend to debugging and I have to fall back to trial&error makes me nervous...

Solution 3:[3]

I wrote following method to convert a ClientResponse to a ServerResponse:

private static Mono<ServerResponse> fromClientResponse(ClientResponse clientResponse){
    return ServerResponse.status(clientResponse.statusCode())
                         .headers(headerConsumer -> clientResponse.headers().asHttpHeaders().forEach(headerConsumer::addAll))
                         .body(clientResponse.bodyToMono(String.class), String.class);
}

Solution 4:[4]

Pure conversion, I come up with the intention of not loading body to memory each time, looks as follows:

Mono<ServerResponse> monoResponse = 
  webClient
   .method(method)
   .uri(uri)
   .headers(headers)
   .body(someBodyInserter)

   // pass-through
   .retrieve()
   .toEntityFlux(DataBuffer.class)
   .flatMap(response -> ServerResponse
     .status(response.getStatusCode())
     .headers(respHeaders -> filterResponseHeaders(response.getHeaders(), respHeaders))
     .body(BodyInserters.fromDataBuffers(response.getBody())));

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ESala
Solution 2 Frischling
Solution 3 Joker
Solution 4 dpedro