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 DataBuffer
s (also ByteBuffer
s). Is there a nice way to do that?