Calling all Spring Framework experts,
Scenario
User A performs
GETrequest to Spring Controller which makes anotherGETrequest 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
@Asyncon a Service which containsRestTemplateapproach - throwsNullPointerExceptionduringbyte copy operationas response output buffer isnull(at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:561)). Service -downloadmethod.StreamingResponseBodyalso doesn't resolve concurrent download issue even when wrapped in@Asyncon service level withAsyncResult<StreamingResponseBody>return. Service -downloadAsyncmethod.
Maybe someone knows a better way of doing this in Spring?
Approaches
RestTemplatewhich performsFileCopyUtils.copy(downloadResponse.getBody(), userResponse.getOutputStream());withinResponseExtractor.HttpsURLConnectionwhich 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;
}
}