2

Here's a part of a Spring Web MVC app using WebClient for its OAuth2 client integration, its purpose being to proxy some requests to and from the resource server with the appropriate authorization headers, hacked together to pipe data from an async Flux<DataBuffer> to a synchronous OutputStream. I did this because I couldn't find a way to make Spring magically handle a Mono<ResponseEntity<Flux<DataBuffer>>> as a return type. I mean it does something, but not what I want from it, which is to stream raw binary data.

@RestController
public class ResourcePassthroughController {

    private final WebClient webClient;
    private final AntPathMatcher pathMatcher = new AntPathMatcher();
    private final String resourceServerUri;
    private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();

    public ResourcePassthroughController(
            WebClient webClient,
            @Value("${app.urls.my-client-resource-server}") String uri) {
        this.webClient = webClient;
        this.resourceServerUri = uri;
    }

    @RequestMapping(value = "/resource/**")
    // public Mono<ResponseEntity<Flux<DataBuffer>>> getResource(
    public ResponseEntity<StreamingResponseBody> getResource(
            @RegisteredOAuth2AuthorizedClient("my-client") OAuth2AuthorizedClient authorizedClient,
            HttpServletRequest req) throws IOException {
        String resourcePath = "/" + pathMatcher.extractPathWithinPattern("/resource/**", req.getServletPath()) // req.getPathInfo() // ?
                + (req.getPathInfo() != null ? req.getPathInfo() : "")
                + (req.getQueryString() != null ? "?" + req.getQueryString() : "");

        // Mono<ResponseEntity<Flux<DataBuffer>>> mono = this
        //     .webClient
        //     .method(HttpMethod.valueOf(req.getMethod()))
        //     .uri(this.resourceServerUri + resourcePath)
        //     .attributes(oauth2AuthorizedClient(authorizedClient))
        //         // .accept(MediaType.APPLICATION_JSON)
        //     .retrieve()
        //     .toEntityFlux(DataBuffer.class);
        // return mono;

        ResponseEntity<Flux<DataBuffer>> responseEntity = this
            .webClient
            .method(HttpMethod.valueOf(req.getMethod()))
            .uri(this.resourceServerUri + resourcePath)
            .body(BodyInserters.fromDataBuffers(DataBufferUtils.read(new InputStreamResource(req.getInputStream()), bufferFactory, 4096)))
            .attributes(oauth2AuthorizedClient(authorizedClient))
            .retrieve()
            .toEntityFlux(DataBuffer.class)
            .block()
            ;
        responseEntity
            .getBody()
            // .timeout(Duration.ofSeconds(90), Flux.empty());
            .timeout(Duration.ofSeconds(90)) // stop reading after 90 seconds and proapagate TimeoutException ?
            ;

        HttpHeaders headers = responseEntity.getHeaders();

        StreamingResponseBody streamer = (outputStream) -> {
            Flux<DataBuffer> flux = DataBufferUtils
            .write(responseEntity.getBody(), outputStream)
            .publish()
            .autoConnect(2); // expect two subscribers (.subscribe and .blockLast)
            flux.subscribe(DataBufferUtils.releaseConsumer()); // resolve MEMORY LEAK
            // flux.timeout(Duration.ofSeconds(120), Flux.error(() -> new IOException("Flux proxy write timeout"))); // stop writing after 120 seconds and propagate exception
            flux.timeout(Duration.ofSeconds(120)); // stop writing after 120 seconds and propagate TimeoutException
            try {
                flux.blockLast(); // once this call returns, the streamer function will follow, so Spring can then close the outputStream it has given us
            } catch (RuntimeException ex) {
                Throwable cause = ex.getCause();
                if (cause instanceof TimeoutException) {
                    throw new IOException("Flux proxy timeout",ex.getCause());
                }
                throw ex;
            }
        };
        return ResponseEntity.ok().headers(headers)
                // .contentLength(headers.getContentLength())
                // .contentType(headers.getContentType())
                .body(streamer);
    }
}

However when I switched on over to WebFlux, Netty happened to do just fine with streaming it:

@RestController
public class ResourcePassthroughController {

    private final WebClient webClient;

    @Value("${app.main-oauth2-client-registration}")
    String oauth2ClientRegistration;

    public ResourcePassthroughController(WebClient webClient) {
        this.webClient = webClient;
    }
    @RequestMapping(value = "/resource/{*path}")
    public Mono<ResponseEntity<Flux<DataBuffer>>> getResource( // this return type does NOT work with servlet, may be a missing decoder
        // @RegisteredOAuth2AuthorizedClient("${app.main-oauth2-client-id}") OAuth2AuthorizedClient authorizedClient,
        ServerHttpRequest request,
        @PathVariable("path") String resourcePath,
        @RequestParam MultiValueMap<String,String> queryMap) {

        return this.webClient
            .method(request.getMethod())
            .uri(builder -> builder
                 .path(resourcePath) // append to path defined in WebClientConfig
                 .queryParams(queryMap)
                 .build()
                 )
            .body(BodyInserters.fromDataBuffers(request.getBody()))
            .attributes(clientRegistrationId(oauth2ClientRegistration)) // OAuth2AuthorizedClient extracted from current logged-in user
            .retrieve()
            .toEntityFlux(DataBuffer.class)
            .timeout(Duration.ofSeconds(90))
            ;
    }
}

But for now I would really like to stay with servlet and somehow make the async part of it properly Flux around my DataBuffers (also ByteBuffers). Is there a nice way to do that?

chaff
  • 21
  • 1
  • You have to be aware that using the reactive bits in a servlet environment kind-of works but isn't (fully) reactive as it only leverages the async part of the Servlet API to mimic things. Which also means you won't be able to utilize the full reactive possibilities as you can with a reactive container like Netty (else why would we nee a reactive runtime if you can do the same with a plain Tomcat?!). – M. Deinum May 12 '21 at 12:44

0 Answers0