title: ClientAbortException with Spring Boot on downloading large files
body: <p>Sometimes when downloading a large file (~1GB) from my Spring Boot application a ClientAbortException occurs. I am using &quot;org.springframework.boot:spring-boot-starter-web:2.3.4.RELEASE&quot; from maven central.</p>
  <p><strong>reproducability</strong><br />
  I could only reproduce it in Google Chrome. In Edge (although based on chrome) it works all the time). Id only does fail occasionally. I also tried to download multiple files at the same time to increase the download time and provoke a timeout, but does not guarantee a failure. sometimes a file works, sometimes not. But most of the time the first download after restarting the server application fails (if done in chrome)</p>
  <p><strong>Stacktrace</strong></p>
  <blockquote>
  <p>org.apache.catalina.connector.ClientAbortException: java.io.IOException: Connection reset by peer
  at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:351)
  at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:776)
  at org.apache.catalina.connector.OutputBuffer.append(OutputBuffer.java:681)
  at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:386)
  at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:364)
  at org.apache.catalina.connector.CoyoteOutputStream.write(CoyoteOutputStream.java:96)
  at org.springframework.util.StreamUtils.copy(StreamUtils.java:166)
  at org.springframework.http.converter.ResourceHttpMessageConverter.writeContent(ResourceHttpMessageConverter.java:137)
  at org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:129)
  at org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:45)
  at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:227)
  at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:280)
  at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:219)
  at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82)
  at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:123)
  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
  at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
  at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
  at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
  at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
  at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:626)
  at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
  at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
  at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
  at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
  at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
  at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
  at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
  at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
  at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
  at java.base/java.lang.Thread.run(Unknown Source)
  Caused by: java.io.IOException: Connection reset by peer
  at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method)
  at java.base/sun.nio.ch.SocketDispatcher.write(Unknown Source)
  at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(Unknown Source)
  at java.base/sun.nio.ch.IOUtil.write(Unknown Source)
  at java.base/sun.nio.ch.IOUtil.write(Unknown Source)
  at java.base/sun.nio.ch.SocketChannelImpl.write(Unknown Source)
  at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:138)
  at org.apache.tomcat.util.net.NioBlockingSelector.write(NioBlockingSelector.java:101)
  at org.apache.tomcat.util.net.NioSelectorPool.write(NioSelectorPool.java:152)
  at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1253)
  at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:764)
  at org.apache.tomcat.util.net.SocketWrapperBase.writeBlocking(SocketWrapperBase.java:584)
  at org.apache.tomcat.util.net.SocketWrapperBase.write(SocketWrapperBase.java:528)
  at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:546)
  at org.apache.coyote.http11.filters.IdentityOutputFilter.doWrite(IdentityOutputFilter.java:73)
  at org.apache.coyote.http11.Http11OutputBuffer.doWrite(Http11OutputBuffer.java:193)
  at org.apache.coyote.Response.doWrite(Response.java:601)
  at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:339)
  ... 57 more</p>
  </blockquote>
  <p><strong>My Setup</strong><br />
  The way my servers are set up is the following:
  VM1 has a nginx running and proxies the call to VM2 with the application.
  the application runs in a docker container with <code>openjdk:11-jre</code> as base image</p>
  <p><em>nginx config for the proxy part</em></p>
  <pre><code>proxy_pass      http://10.0.0.3:2080/;
  proxy_read_timeout 1d;
  </code></pre>
  <p>Before implementing it with spring boot, the server was written with vertx. The vertx implementation did not have those issues. The vertx implementation did not set the <code>content-length</code> header.</p>
  <p><strong>Code</strong></p>
  <pre><code>@GetMapping(&quot;{fileId:.+}&quot;)
  fun getFile(@PathVariable fileId: String): ResponseEntity&lt;PathResource&gt; {
      var asMap = fileIdCache.asMap()
      if (!asMap.containsValue(fileId)) {
          FileLoader().getAllFiles()
                  .map { FileLoader.FILEPATH.relativize(it).joinToString(&quot;/&quot;, transform = java.nio.file.Path::toString) }
                  .forEach { fileIdCache[it] }
          asMap = fileIdCache.asMap()
      }
      val filename = asMap.toList()
                             .filter { it.second == fileId }
                             .map { it.first }
                             .firstOrNull() ?: throw NotFoundException(fileId)
      val path = FileLoader.FILEPATH.resolve(filename)
      if (!Files.exists(path)) {
          throw NotFoundException(fileId)
      }
      info(&quot;requested File: $path&quot;)
  
      val encoded = URLEncoder.encode(path.fileName.toString(), Charsets.UTF_8).replace(&quot;+&quot;, &quot;%20&quot;)
      val probeContentType = Files.probeContentType(path)
      info(&quot;probed content: $probeContentType&quot;)
      val type = probeContentType?.let(MediaType::valueOf) ?: MediaType.APPLICATION_OCTET_STREAM
  
      val resource = PathResource(path)
      return ResponseEntity.ok()
              .header(&quot;Content-Disposition&quot;, &quot;attachment;filename=$encoded&quot;)
              .contentType(type)
              .body(resource)
  }
  </code></pre>
  <p><strong>Response headers</strong></p>
  <pre><code>accept-ranges: bytes
  cache-control: max-age=2592000
  content-disposition: attachment;filename=video.mkv
  content-length: 1084320986
  content-type: application/octet-stream
  date: Sat, 10 Oct 2020 16:28:26 GMT
  expires: Mon, 09 Nov 2020 16:28:26 GMT
  referrer-policy: origin
  server: nginx
  status: 200
  strict-transport-security: max-age=31536000; includeSubDomains; preload
  vary: Origin
  vary: Access-Control-Request-Method
  vary: Access-Control-Request-Headers
  </code></pre>
  <p>Old response headers from vertx implementation</p>
  <pre><code>cache-control: max-age=2592000
  content-disposition: attachment;filename=&quot;video.mkv&quot;
  content-type: application/octet-stream
  date: Sat, 10 Oct 2020 17:45:51 GMT
  expires: Mon, 09 Nov 2020 17:45:51 GMT
  referrer-policy: origin
  server: nginx
  status: 200
  strict-transport-security: max-age=31536000; includeSubDomains; preload
  </code></pre>
  <p>I found some solutions mentioning to add the content-length header (which is there and correct) or add the <code>proxy_read_timeout</code> in the nginx configuration, which did not help.
  I also tried different jdks/jres, does not change anything.</p>