Spring REST endpoint returning StreamingResponseBody: AsyncRequestTimeoutException after 30 seconds

烈酒焚心 提交于 2021-02-11 15:14:32

问题


I have the same problem as described here and here.

I tried the answers given and combinations thereof but none solved my issue.

When I tried this answer, after 30 seconds, instead of the timeout, the download restarted from the beginning and then, after 30 more seconds, then it timed out.

I'm testing by visiting the REST endpoint in Google Chrome and trying to download a file from there.

Here I have the project that displays this error.

Thanks in advance.

Edit: here's the source:

src\main\java\io\github\guiritter\transferer_local\TransfererLocalApplication.java

package io.github.guiritter.transferer_local;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class TransfererLocalApplication {

    public static void main(String[] args) {
        SpringApplication.run(TransfererLocalApplication.class, args);
    }
}

src\main\java\io\github\guiritter\transferer_local\DefaultController.java

package io.github.guiritter.transferer_local;

import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.webmvc.RepositoryRestController;
// import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RepositoryRestController
@RequestMapping("api")
public class DefaultController {

    @Value("${fileName}")
    private String fileName;

    @Value("${filePath}")
    private String filePath;

    @GetMapping("download")
    public StreamingResponseBody downloadHub(HttpServletResponse response) throws IOException {
        File file = new File(filePath + fileName);
        response.setContentType(APPLICATION_OCTET_STREAM_VALUE);
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
        response.setHeader("Content-Length", file.length() + "");
        InputStream inputStream = new FileInputStream(file);
        return outputStream -> {
            int nRead;
            byte[] data = new byte[1024*1024];
            while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                outputStream.write(data, 0, nRead);
            }
            inputStream.close();
        };
    }

    // @GetMapping("download")
    // public ResponseEntity<StreamingResponseBody> downloadHub(HttpServletResponse response) throws IOException {
    //  File file = new File(filePath + fileName);
    //  response.setContentType(APPLICATION_OCTET_STREAM_VALUE);
    //  response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
    //  response.setHeader("Content-Length", file.length() + "");
    //  InputStream inputStream = new FileInputStream(file);

    //  return ResponseEntity.ok(outputStream -> {
    //      int nRead;
    //      byte[] data = new byte[1024*1024];
    //      while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
    //          outputStream.write(data, 0, nRead);
    //      }
    //      inputStream.close();
    //  });
    // }
}

src\main\java\io\github\guiritter\transferer_local\AsyncConfiguration.java

package io.github.guiritter.transferer_local;

// import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    @Bean(name = "taskExecutor")
    public AsyncTaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(Integer.MAX_VALUE);
        executor.setThreadNamePrefix("io.github.guiritter.transferer_local.async_executor_thread.");
        return executor;
    }

    /** Configure async support for Spring MVC. */
    @Bean
    public WebMvcConfigurer webMvcConfigurerAdapter(
            AsyncTaskExecutor taskExecutor) {
        return new WebMvcConfigurer() {

            @Override
            public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
                configurer
                        .setDefaultTimeout(Long.MAX_VALUE)
                        .setTaskExecutor(taskExecutor);
                configureAsyncSupport(configurer);
            }
        };
    }

    // @Autowired
    // private AsyncTaskExecutor taskExecutor;

    // /** Configure async support for Spring MVC. */
    // @Bean
    // public WebMvcConfigurer webMvcConfigurerAdapter() {
    //  return new WebMvcConfigurer() {

    //      @Override
    //      public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    //          configurer
    //                  .setDefaultTimeout(Long.MAX_VALUE)
    //                  .setTaskExecutor(taskExecutor);
    //          configureAsyncSupport(configurer);
    //      }
    //  };
    // }
}

src\main\java\io\github\guiritter\transferer_local\MyConfiguration.java

package io.github.guiritter.transferer_local;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
@EnableTransactionManagement
@EnableAsync
public class MyConfiguration implements WebMvcConfigurer {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setDefaultTimeout(-1);
    }
}

src\main\resources\application.properties

server.port=8081
fileName=large_file_name.txt
filePath=C:\\path\\to\\large\\file\\

# spring.mvc.async.request-timeout = 9223372036854775807
# spring.mvc.async.request-timeout = 2147483647
spring.mvc.async.request-timeout = -1

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.github.guiritter</groupId>
    <artifactId>transferer-local</artifactId>
    <version>1.0.0</version>
    <name>TransfererLocal</name>
    <description>Enables local network file transfer</description>

    <properties>
        <java.version>14</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-rest-webmvc -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-rest-webmvc</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Update: trying Manuel's answer (commited to branch answer_Manuel):

src\main\java\io\github\guiritter\transferer_local\DefaultController.java

package io.github.guiritter.transferer_local;

import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RepositoryRestController
@RequestMapping("api")
public class DefaultController {

    @Value("${fileName}")
    private String fileName;

    @Value("${filePath}")
    private String filePath;

    @GetMapping("download")
    public WebAsyncTask<ResponseEntity<StreamingResponseBody>> downloadHub(HttpServletResponse response) throws IOException {
        File file = new File(filePath + fileName);
        response.setContentType(APPLICATION_OCTET_STREAM_VALUE);
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
        response.setHeader("Content-Length", file.length() + "");
        InputStream inputStream = new FileInputStream(file);

        return new WebAsyncTask<ResponseEntity<StreamingResponseBody>>(Long.MAX_VALUE, () ->

            ResponseEntity.<StreamingResponseBody>ok(outputStream -> {

                int nRead;
                byte[] data = new byte[1024*1024];
                while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                    outputStream.write(data, 0, nRead);
                }
                inputStream.close();
            })
        );
    }
}

It threw AsyncRequestTimeoutException and this:

java.lang.IllegalArgumentException: Cannot dispatch without an AsyncContext
        at org.springframework.util.Assert.notNull(Assert.java:198) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.dispatch(StandardServletAsyncWebRequest.java:131) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.WebAsyncManager.setConcurrentResultAndDispatch(WebAsyncManager.java:391) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$2(WebAsyncManager.java:315) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.lambda$onError$0(StandardServletAsyncWebRequest.java:146) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1510) ~[na:na]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.onError(StandardServletAsyncWebRequest.java:146) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.apache.catalina.core.AsyncListenerWrapper.fireOnError(AsyncListenerWrapper.java:49) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.catalina.core.AsyncContextImpl.setErrorState(AsyncContextImpl.java:422) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:239) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:237) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1591) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) ~[na:na]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]

Update: trying Manuel's updated answer (commited to branch answer_Manuel_2020-04-06):

src\main\java\io\github\guiritter\transferer_local\DefaultController.java

package io.github.guiritter.transferer_local;

import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RepositoryRestController
@RequestMapping("api")
public class DefaultController {

    @Value("${fileName}")
    private String fileName;

    @Value("${filePath}")
    private String filePath;

    @GetMapping("download")
    public ResponseEntity<StreamingResponseBody> downloadHub() throws IOException {
        File file = new File(filePath + fileName);
        InputStream inputStream = new FileInputStream(file);
        return ResponseEntity
                .ok()
                .contentType(APPLICATION_OCTET_STREAM)
                .header("Content-Disposition", "attachment; filename=" + fileName)
                .header("Content-Length", file.length() + "")
                .<StreamingResponseBody>body(outputStream -> {
                    int nRead;
                    byte[] data = new byte[1024*1024];
                    while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                        outputStream.write(data, 0, nRead);
                    }
                    inputStream.close();
                });
    }
}

回答1:


To fix the problem:

Change @RepositoryRestController to e.g., @RestController.

If you use @RepositoryRestController, the timeout will be set for the RequestMappingHandlerAdapter. But upon requesting the download, the RepositoryRestHandlerAdapter handles the request, as the annotation requires him to.

If you use the @RestController then the (correct) RequestMappingHandlerAdapter will handle the download, with the timeout set to -1.

The original answer:

You could try to declarative/explicit define the timeout, by returning a org.springframework.web.context.request.async.WebAsyncTask.

If offers to set a Callable<V> with a timeout:

Then your DefaultController could look like:

public WebAsyncTask<ResponseEntity<StreamingResponseBody>> downloadHub() throws IOException {
  ...
  new WebAsyncTask<ResponseEntity<StreamingResponseBody>>(myTimeOutAsLong, callable);
}

Update:

  1. Please remove the HttpServletResponse parameter from your REST controller method. Just to be sure, that the OutputStream from the HttpServletResponse does not interfere with the OutputStream from StreamingResponseBody.
  2. Regarding the error ´Cannot dispatch without an AsyncContext´: "Cannot dispatch without an AsyncContext" when serving files


来源:https://stackoverflow.com/questions/61030650/spring-rest-endpoint-returning-streamingresponsebody-asyncrequesttimeoutexcepti

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!