Calling all Spring Framework experts,
Scenario
User A performs
GET
request to Spring Controller which makes anotherGET
request to remote host to get a file which content is streamed (buffer byte copy) to initial user A as a response.
PS: Spring Controller acts like a proxy
Using
- Spring WebMVC 4.3.3.RELEASE
- Apache Tomcat 8.5.5
Issue
Seems like there is no Servlet request thread available or something else is blocked...
First user is able to initiate downloading of a file in all cases listed below. Unfortunately, Spring stops to invoke controller download method until first download has been finished (but, sometimes, it invokes it after XX seconds of user's wait time). Tried approaches with
@Async
on a Service which containsRestTemplate
approach - throwsNullPointerException
duringbyte copy operation
as response output buffer isnull
(at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:561)
). Service -download
method.StreamingResponseBody
also doesn't resolve concurrent download issue even when wrapped in@Async
on service level withAsyncResult<StreamingResponseBody>
return. Service -downloadAsync
method.
Maybe someone knows a better way of doing this in Spring?
Approaches
RestTemplate
which performsFileCopyUtils.copy(downloadResponse.getBody(), userResponse.getOutputStream());
withinResponseExtractor
.HttpsURLConnection
which performsFileCopyUtils.copy(downloadResponse.getBody(), userResponse.getOutputStream());
within controller's returnStreamingResponseBody
All of the listed approaches do work, they transmit the file content from one server response's InputStream
to the user's response OutputStream
.
However, there is an issue with concurrent downloads (Spring stops invoking controller's download method).
Sources
Private info (urls, authentication and etc...) was replaced, code is written strictly for question demonstration purpose
Dispatcher
Constructed by extending AbstractAnnotationConfigDispatcherServletInitializer
.
Controller
Contains 3 endpoints for different approaches.
@RequestMapping(value = "/download/way1", method = RequestMethod.GET)
public void requestDownloadPage(final HttpServletResponse downloadResponse, final HttpServletRequest downloadRequest) throws ExecutionException, InterruptedException, URISyntaxException {
dwn.download(downloadResponse);
}
@RequestMapping(value = "/download/way2", method = RequestMethod.GET)
public StreamingResponseBody requestDownloadPage2(final HttpServletResponse response) throws ExecutionException, InterruptedException, URISyntaxException {
return dwn.downloadAsync(response).get();
}
@RequestMapping(value = "/download/way3", method = RequestMethod.GET)
public StreamingResponseBody requestDownloadPage3(final HttpServletResponse response) throws ExecutionException, InterruptedException, URISyntaxException, IOException {
return outputStream -> {
URL url = new URL("https://some/url/path/veryBig.zip");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setDoOutput(false);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setRequestProperty ("Authorization", "Basic " + Base64.getEncoder().encodeToString("username:password".getBytes(StandardCharsets.UTF_8)));
connection.setRequestMethod("GET");
response.addHeader(HttpHeaders.CONTENT_TYPE, connection.getHeaderField(HttpHeaders.CONTENT_TYPE));
response.addHeader(HttpHeaders.CONTENT_LENGTH, connection.getHeaderField(HttpHeaders.CONTENT_LENGTH));
String disposition = connection.getHeaderField(HttpHeaders.CONTENT_DISPOSITION);
if (disposition != null && !disposition.isEmpty()) {
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, disposition);
}
try (InputStream inputStream = connection.getInputStream();) {
FileCopyUtils.copy(inputStream, outputStream);
} catch (Throwable any) {
// failed
}
};
}
Service
public void download(HttpServletResponse downloadResponse) {
final RestTemplate restTemplate = new RestTemplate();
RequestCallback requestCallback = new RequestCallback() {
@Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("username:password".getBytes(StandardCharsets.UTF_8)));
}
};
ResponseExtractor<Void> responseExtractor = response -> {
List<String> type = response.getHeaders().get(HttpHeaders.CONTENT_TYPE);
List<String> length = response.getHeaders().get(HttpHeaders.CONTENT_LENGTH);
List<String> disposition = response.getHeaders().get(HttpHeaders.CONTENT_DISPOSITION);
downloadResponse.addHeader(HttpHeaders.CONTENT_TYPE, type.get(0));
downloadResponse.addHeader(HttpHeaders.CONTENT_LENGTH, length.get(0));
if (disposition != null && !disposition.isEmpty()) {
downloadResponse.addHeader(HttpHeaders.CONTENT_DISPOSITION, disposition.get(0));
}
FileCopyUtils.copy(response.getBody(), downloadResponse.getOutputStream());
return null;
};
restTemplate.execute("https://some/url/path/veryBig.zip",
HttpMethod.GET,
requestCallback,
responseExtractor);
}
@Async
public Future<StreamingResponseBody> downloadAsync(HttpServletResponse response) {
final RestTemplate restTemplate = new RestTemplate();
RequestCallback requestCallback = new RequestCallback() {
@Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("username:password".getBytes(StandardCharsets.UTF_8)));
}
};
ResponseExtractor<StreamingResponseBody> responseExtractor = responseOwnCloud -> {
List<String> type = responseOwnCloud.getHeaders().get(HttpHeaders.CONTENT_TYPE);
List<String> length = responseOwnCloud.getHeaders().get(HttpHeaders.CONTENT_LENGTH);
List<String> disposition = responseOwnCloud.getHeaders().get(HttpHeaders.CONTENT_DISPOSITION);
response.setHeader(HttpHeaders.CONTENT_TYPE, type.get(0));
response.setHeader(HttpHeaders.CONTENT_LENGTH, length.get(0));
if (disposition != null && !disposition.isEmpty()) {
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, disposition.get(0));
}
return outputStream -> {
FileCopyUtils.copy(responseOwnCloud.getBody(), response.getOutputStream());
};
};
return new AsyncResult<>(restTemplate.execute("https://some/url/path/veryBig.zip",
HttpMethod.GET,
requestCallback,
responseExtractor));
}
Spring Async Configuration
@Configuration
@ComponentScan(basePackages = { "some.service"})
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("AsyncExec-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}