SocketTimeoutException while downloading large file and this behavior is inconsistent among different browsers
Description of the bug
Using Vaadin 24.9.3 with Spring-Boot 3.5.7 and Tomcat 10.1.48, when i try to download a file using DownloadHandler.fromInputStream API, the download is being interrupted after some time.
On firefox browser, it is interrupted after 30seconds. On chromium based browsers (tried chrome and brave), the download is interrupted after 1GB of data downloaded.
Application is deployed to a debian server behind a nginx proxy as a war file. Nginx proxy has all timeout set to a high value, so timeout is not happening because of nginx:
send_timeout 300;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
Regarding tomcat and spring configuration, i tried to use high values for timeouts or infinite timeouts, but they have no effect:
tomcat:
connection-timeout: 900000 (default is 60000).
I have found no default timeout configuration of stack that i am using, which could cause a 30second timeout while download a file. Moreover, this behavior is inconsistent among different types of browsers.
Looking at Network inspector in firefox, the request status is 200, but after 30 seconds it gets NS_BINDING_ABORTED. The download is not canceled by user. I tried to increase default timeouts in firefox too, having no effect:
I also tried to use different java/tomcat versions.
What is also interesting is that uploading a large file using Upload component in Vaadin completes without issue. The problem is happening only during download.
Vaadin java code:
hiddenDownloadAnchor.setHrefAndDownload(DownloadHandler.fromInputStream(
downloadEvent -> new DownloadResponse(
new FileInputStream(file),
name,
type,
length))); // hiddenDownloadAnchor is an hidden anchor
Content length is set correctly, also with mime type. I also tried to use null or -1 values to indicate unknown size or auto-decide mime type, having no effect.
I am frustrated by having no clue about what might cause this weird behavior. I hope someone could help be debug this issue. Thanks in advance.
Looking at stack trace, where the exception is thrown in tomcat: NioSocketWrapper.doWrite(NioEndpoint.java:1410) i can see there is a timeout, because of something, but looking at timeout variable in debug, which is retrieved from getWriteTimeout(), is larger than 30 seconds (even defaut is 60), so I have no clue, why this happens in firefox consistently after 30 seconds, and in chromium based browsers after downloading 1GB (exceeding 30 seconds: downloading for about 5 minutes).
Complete stack trace:
org.apache.catalina.connector.ClientAbortException: java.net.SocketTimeoutException
at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:342)
at org.apache.catalina.connector.OutputBuffer.appendByteArray(OutputBuffer.java:747)
at org.apache.catalina.connector.OutputBuffer.append(OutputBuffer.java:675)
at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:377)
at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:355)
at org.apache.catalina.connector.CoyoteOutputStream.write(CoyoteOutputStream.java:102)
at com.vaadin.flow.server.streams.TransferUtil.transfer(TransferUtil.java:97)
at com.vaadin.flow.server.streams.InputStreamDownloadHandler.handleDownloadRequest(InputStreamDownloadHandler.java:101)
at com.vaadin.flow.server.streams.DownloadHandler.handleRequest(DownloadHandler.java:104)
at com.vaadin.flow.server.communication.StreamRequestHandler.callElementResourceHandler(StreamRequestHandler.java:194)
at com.vaadin.flow.server.communication.StreamRequestHandler.handleRequest(StreamRequestHandler.java:119)
at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1879)
at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:398)
at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:106)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:612)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:394)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:323)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:268)
at org.springframework.web.servlet.mvc.ServletForwardingController.handleRequestInternal(ServletForwardingController.java:142)
at org.springframework.web.servlet.mvc.AbstractController.handleRequest(AbstractController.java:178)
at org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter.handle(SimpleControllerHandlerAdapter.java:51)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:124)
at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:666)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1776)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:975)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:493)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
at java.base/java.lang.Thread.run(Thread.java:1474)
Caused by: java.net.SocketTimeoutException
at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1410)
at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:732)
at org.apache.tomcat.util.net.SocketWrapperBase.writeBlocking(SocketWrapperBase.java:572)
at org.apache.tomcat.util.net.SocketWrapperBase.write(SocketWrapperBase.java:520)
at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:548)
at org.apache.coyote.http11.filters.ChunkedOutputFilter.doWrite(ChunkedOutputFilter.java:111)
at org.apache.coyote.http11.Http11OutputBuffer.doWrite(Http11OutputBuffer.java:193)
at org.apache.coyote.Response.doWrite(Response.java:628)
at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:330)
... 70 more
Expected behavior
Downloading large files should complete without issue and should not timeout on firefox browser after 30 seconds or after 1GB downloaded on chromium based browsers.
Minimal reproducible example
Add view to vaadin with an anchor and initiate download of large file (exceeding 1GB):
hiddenDownloadAnchor.setHrefAndDownload(DownloadHandler.fromInputStream(
downloadEvent -> new DownloadResponse(
new FileInputStream(file),
name,
type,
length)));
Application is spring boot with tomcat, deployed as war behind nginx proxy.
Versions
- Vaadin / Flow version: 24.9.3
- Java version: 25/21
- Browser version (if applicable): Firefox 145.0b6 or Brave 1.83.120 (Chromium: 141.0.7390.122)
- Application Server (if applicable): Tomcat 10.1.48 (also tried older: 10.1.46)
EDIT: investigation update
Came across this old nginx issue citing:
Hi, we tried nginx version 1.6.2 till version 1.12.2 and have a problem when used as proxy before >artifactory. Downloads get interrupted at 1GB.
This behavior depends on the internal VLAN. On one VLAN this always happens. On an other VLAN it >never happens. This is size limited, not time limited. From some network it stops after 30 seconds >and from one other slow network it stops after 13 minutes.
We made a minimal proxy setup with apache and this works with all VLANs. This is why we expect it has >something to do with nginx or the combination of nginx and TCP/IP stack of linux.
In wireshark we see "TCP Dup ACK" on the client side sent to the nginx server.
Wget fails with connection closed at byte 1083793011 but continues download with partial content. >docker can't handle this and our customers can't download docker images with layers greater 1 GB.
With suggestion:
The 1GB limit suggests that the problem is due to proxy_max_temp_file_size. It is one gigabyte by >default, and if the limit is reached, nginx will stop reading from the backend till all disk-buffered >data are sent to the client. This in turn can result in a send timeout on the backend side.
Please check nginx and your backend logs to see what happens here. Likely there are something like "upstream prematurely closed connection" in nginx error log, and send timeouts in your backend logs. Alternatively, just check if proxy_max_temp_file_size 0; helps (this will disable disk buffering completely).
If the above suggestion is true, the two possible solutions are: Tune proxy_max_temp_file_size. Consider either configuring the limit above the size of all expected responses, or small enough for your backend to don't time out. In particular, proxy_max_temp_file_size 0; might be a good choice when proxying large files. Tune your backend timeouts appropriately.
Changing this proxy_max_temp_file_size to 0 helps with downloads on chromium based browsers, but this issue persist on firefox.
Any more information on the setup and things between the server and browser? I can't get FireFox 144.0.2 (64-bit) to fail on 30s mark. Only on the server timeout of 60s.
Have you checked if http/3 is enabled?
Some servers or network appliances mishandle QUIC, causing interrupted downloads.
Go to about:config
Search for network.http.http3.enabled
Set it to false
Restart Firefox and retry the download.
For Vaadin, we use defualt Transport.WEBSOCKET_XHR configuration, which is XHR from client to server and WS from server to client.
On server, nginx is configured to use http 1.1 or WS only:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_http_version 1.1;
Tomcat is also configured to use http 1.1 only using spring-boot properties file:
server:
http2:
enabled: false
I tried setting network.http.http3.enabled to false from true in FF, but same result.
Still couldn't replicate the 30s issue. Would it be possible to have a minimal localhost setup on top of a starter with nginx configuration that would replicate the issue?
I just managed to replicate the issue (firefox failing to download large file) on localhost using exactly the same nginx configuration in nginx.conf file as we use on server except https - using latest stable nginx 1.28.0 on Windows platform (nothing else is changed):
worker_processes auto;
events {
worker_connections 1024;
}
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
limit_rate 5m;
limit_req_zone $binary_remote_addr zone=limitzone:10m rate=300r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
keepalive_requests 150;
send_timeout 75s;
proxy_connect_timeout 75s;
proxy_send_timeout 75s;
proxy_read_timeout 75s;
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 300;
client_max_body_size 2100M;
server {
listen 808;
server_name localhost;
location / {
limit_conn addr 20;
limit_req zone=limitzone burst=50 nodelay;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_connect_timeout 75s;
proxy_send_timeout 75s;
proxy_read_timeout 75s;
proxy_http_version 1.1;
add_header X-Frame-Options SAMEORIGIN always;
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains' always;
proxy_pass http://127.0.0.1:8080/;
}
}
}
where http://127.0.0.1:8080/ is spring boot vaadin application running. You can even use default configuration properties, just proxy requests to the right port. Still, it blows my mind why chromium browsers have no issue.
I still don't get the 30s failure, but I did note that you are using 145.0b6 which is a beta version where as I have the stable 145.0 Could you update the version to a stable one if that would help? Also which os is the download made from?
I tried 145 64-bit FF, but nothing changed. Download is made from Windows 10 or Windows 11 devices.
If you tried nginx with default configuration or the one i provided, and still you can download with FF, it must be something in my vaadin application configuration then.
I am posting additional configuration i use with some obfuscated parameters:
server:
http2:
enabled: false
port: ${PORT:8080}
servlet:
session:
timeout: 1h
persistent: false
cookie:
name: xxx
tomcat:
connection-timeout: 120000
max-part-count: 30
max-part-header-size: 2KB
vaadin:
allowed-packages:
- xxx
launch-browser: true
spring:
servlet:
multipart:
max-file-size: 2100MB
max-request-size: 2150MB
datasource:
url: xxx
username: xxx
password: xxx
@SpringBootApplication
@Theme(value = "xxx")
@PWA(name = "xxx", shortName = "xxx", iconPath = "icons/xxx.png", offlinePath = "offline.html")
@Push(value = PushMode.MANUAL)
@EnableAsync
@EnableScheduling
public class Application extends SpringBootServletInitializer implements AppShellConfigurator {
// dev only
public static void main(final String[] args) throws Exception {
new SpringApplicationBuilder(Application.class)
.listeners(new DatabasePropertiesSetup())
.run(args);
}
}
Any clue what might cause this?
Using old deprecated StreamResource api also makes no change
I could replicate the issue when running in production mode even without nginx in the middle. No idea what is happening here and am trying to look into it. Ofc the recommendation would be to server large files in another way than dynamic download.