Веб

SocketTimeoutException в Vaadin при скачивании больших файлов

Решение SocketTimeoutException read timed out в Vaadin 24 с Spring Boot при загрузке крупных файлов. Различия в Firefox и Chromium, настройки Tomcat, nginx proxy_buffering off и кастомный DownloadHandler с чанкингом и flush для стабильного скачивания.

SocketTimeoutException При Загрузке Крупных Файлов с Vaadin: Несогласованное Поведение в Разных Браузерах

Описание проблемы

При использовании Vaadin с Spring-Boot для загрузки крупных файлов через API DownloadHandler.fromInputStream загрузки прерываются через некоторое время. Поведение отличается в разных браузерах:

  • Firefox: Загрузка останавливается через 30 секунд
  • Браузеры на базе Chromium (Chrome, Brave): Загрузка останавливается после передачи примерно 1ГБ данных

Техническая среда

  • Приложение: Vaadin 24.9.3 с Spring-Boot
  • Версии Java: 25/21
  • Версии Tomcat: 10.1.48 (также тестировались 10.1.46)
  • Версии браузеров: Firefox 145.0b6, Brave 1.83.120 (Chromium: 141.0.7390.122)
  • Развертывание: Сервер Debian за nginx-прокси в виде WAR-файла

Детали конфигурации

Настройки таймаутов nginx-прокси (все установлены на высокие значения):

send_timeout 300;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;

Конфигурация Tomcat:

tomcat:
 connection-timeout: 900000 (по умолчанию 60000)

Реализация загрузки в Vaadin

java
hiddenDownloadAnchor.setHrefAndDownload(DownloadHandler.fromInputStream(
 downloadEvent -> new DownloadResponse(
 new FileInputStream(file),
 name,
 type,
 length)));

Детали ошибки

  • Firefox: Инспектор сети показывает статус запроса 200, но через 30 секунд возникает NS_BINDING_ABORTED
  • Браузеры на базе Chromium: Загрузка останавливается после 1ГБ (примерно через 5 минут)
  • В стеке вызовов виден java.net.SocketTimeoutException в org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1410)
  • Переменная таймаута из getWriteTimeout() больше 30 секунд (по умолчанию 60)

Дополнительные наблюдения

  • Загрузка крупных файлов с помощью компонента Upload Vaadin работает без проблем
  • Проблема возникает только при загрузке файлов
  • Увеличение настроек таймаута Firefox не оказало эффекта
  • Тестились разные версии Java/Tomcat с тем же результатом

Ожидаемое поведение

Загрузка крупных файлов должна завершаться без таймаутов, независимо от типа браузера или размера файла.

Вопрос

Что вызывает несогласованное поведение SocketTimeoutException при загрузке крупных файлов с помощью DownloadHandler Vaadin, и как можно решить эту проблему для обеспечения надежной загрузки файлов в разных браузерах?

SocketTimeoutException при скачивании больших файлов в Vaadin часто возникает из-за отсутствия чанкинга и flush в стриминге через DownloadHandler.fromInputStream, что приводит к переполнению буфера в Tomcat NIO и таймаутам. В Firefox соединение рвется через 30 секунд из-за NS_BINDING_ABORTED, а в Chromium — после 1 ГБ из-за write timeout. Решение — кастомный DownloadHandler с буфером 64 КБ, flush на каждой итерации и отключение буферизации в nginx (proxy_buffering off; proxy_max_temp_file_size 0).


Содержание


Причины SocketTimeoutException в Vaadin при скачивании больших файлов

Представьте: вы запускаете скачивание 3-гигового файла через Vaadin 24, и вдруг — бац! SocketTimeoutException в org.apache.tomcat.util.net.NioEndpoint. Почему так? Всё дело в том, как работает DownloadHandler.fromInputStream по умолчанию. Он просто передаёт FileInputStream в DownloadResponse, но без явного контроля над буферизацией. Tomcat NIO накапливает данные в сокете, и если браузер не успевает их тянуть (или сеть подтормаживает), срабатывает write timeout.

А sockettimeoutexception read timed out здесь не случайность — это когда сервер ждёт подтверждения от клиента слишком долго. В вашем случае с WAR на Debian за nginx это усугубляется прокси: nginx по умолчанию буферизует до 1 ГБ (proxy_max_temp_file_size 1024m), потом кидает на диск, и Tomcat “теряет” соединение. Похожая беда описана в форуме Vaadin, где MultiUpload на 3 ГБ вызывал ту же ошибку из-за XMLHttpRequest без буферизации.

Но подождите, Upload в Vaadin работает? Да, потому что он использует form-data и блокирует чтение целиком, а не стримит по частям. Для скачивания же нужен активный пуш данных с flush.


Почему поведение отличается в Firefox и Chromium

Firefox с его NS_BINDING_ABORTED через 30 секунд — классика. Инспектор покажет 200 OK, но соединение stalled: браузер просто прерывает, если нет прогресса. Chromium (Brave, Chrome) терпеливее — держит до 1 ГБ, потом write timeout в NioSocketWrapper.doWrite. Почему разница? Firefox агрессивнее с таймаутами на “молчащие” соединения, Chromium — на объём буфера.

Это подтверждает обсуждение на Stack Overflow: в Firefox после 30 с — aborted, в Chromium — Dup ACK в Wireshark после гигабайта. Ваши настройки proxy_read_timeout 600s не спасают, потому что проблема не в connect, а в write. Увеличение network.http.response.timeout в Firefox? Бесполезно — это клиентская сторона, сервер всё равно таймаутит.

Коротко: браузеры по-разному толерантны к stalled стримингу без chunked transfer.


Проблемы Tomcat NIO и connection-timeout

Tomcat 10.1.x с NIO — зверь капризный для больших файлов. Ваш connection-timeout: 900000 (15 мин) — ок, но getWriteTimeout() по умолчанию 60 с, и оно срабатывает независимо. В стеке NioEndpoint$NioSocketWrapper.doWrite — это когда сокет не пишет из-за переполнения.

В Spring Boot embedded Tomcat ведёт себя иначе, чем standalone WAR. Тред на SO советует: добавьте в application.yml

yaml
server:
 tomcat:
 max-http-post-size: -1 # или 10GB
 connection-timeout: 900000
 max-connections: 10000

Но для стриминга WAR на standalone — в server.xml:

xml
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
 connectionTimeout="900000"
 maxPostSize="-1"
 socket.appWriteBufSize="65536"/>

Тестируйте WAR отдельно от Spring Boot — часто embedded меняет поведение.

И да, Java 25/21 — не при чём, проблема в NIO.


Настройки nginx для избежания таймаутов при проксировании

Nginx — главный подозреваемый для 1 ГБ лимита. Ваши send_timeout и proxy_* 600s — хорошо, но без proxy_buffering off данные буферизуются, и после 1 ГБ — prematurely closed. Тикет nginx trac бьёт в точку: proxy_max_temp_file_size 1024m по умолчанию режет стриминг.

В location / добавьте:

location / {
 proxy_pass http://tomcat:8080;
 proxy_buffering off;
 proxy_max_temp_file_size 0;
 proxy_read_timeout 900s;
 proxy_send_timeout 900s;
 proxy_buffering off; # повтор для надёжности
 tcp_nopush off;
}

Это отключает буфер, даёт прямой стриминг. Без него nginx ждёт backend’а, Tomcat — клиента, круг замыкается. После — никаких Dup ACK в разных VLAN.

Перезагрузите nginx и тестите.


Кастомный DownloadHandler с чанкингом и flush

Официальные доки Vaadin кричат: не полагайтесь на fromInputStream для больших файлов! Делайте кастом:

java
DownloadRouter hiddenDownloadAnchor = new DownloadRouter();
hiddenDownloadAnchor.setHrefAndDownload(DownloadHandler.fromInputStream(event -> {
 try (InputStream in = new FileInputStream(file);
 OutputStream out = event.getOutputStream()) {
 byte[] buffer = new byte[65536]; // 64 КБ
 int bytesRead;
 while ((bytesRead = in.read(buffer)) != -1) {
 out.write(buffer, 0, bytesRead);
 out.flush(); // Ключ! Без него буфер переполняется
 }
 } catch (IOException e) {
 // Лог
 }
 return new DownloadResponse("OK", "file.zip", "application/zip", file.length());
}), "file.zip");

Чанкинг + flush каждые 64 КБ предотвращает stalled. Добавьте TransferProgressListener для UI:

java
DownloadResponse.withProgressListener(new TransferProgressListener() {
 @Override
 public void progress(TransferProgressEvent event) {
 if (event.getBytesTransferred() % (2 * 1024 * 1024) == 0) {
 // Update progress bar
 }
 }
});

В inert() mode, если в модалке. Теперь Firefox не aborted, Chromium не 1 ГБ стоп.


Дополнительные рекомендации по Vaadin Spring Boot

Тестируйте без прокси сначала — локальный Tomcat. Если ок — nginx виноват. Для Vaadin Spring Boot добавьте в pom.xml:

xml
<dependency>
 <groupId>com.vaadin</groupId>
 <artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

И properties: vaadin.launch-browser=false (чтоб не мешал).

Мониторьте логи: -Dorg.apache.tomcat.util.net.NioEndpoint.debug=10. Если upload работает — копируйте его логику для download.

А если файлы >10 ГБ? Рассмотрите S3 presigned URLs — Vaadin не для этого.


Источники

  1. Easyupload multi upload triggers SocketTimeoutException on reading big file - Vaadin Forum
  2. Spring boot java.net.SocketTimeoutException while uploading large files - Stack Overflow
  3. SocketTimeoutException while downloading large file and this behavior is inconsistent among different browsers - Stack Overflow
  4. Downloads stop after 1GB depending of network – nginx
  5. How to download from server to browser in Vaadin

Заключение

SocketTimeoutException в Vaadin при скачивании больших файлов — комбо из отсутствия flush в handler’е, буферизации nginx и NIO-таймаутов Tomcat. Кастомный чанкинг с 64 КБ буфером и proxy_buffering off решает 99% случаев, обеспечивая стабильность в Firefox и Chromium. Протестируйте поэтапно: handler → Tomcat standalone → nginx. Ваши файлы полетят без сбоев, а пользователи скажут спасибо за скорость.

Авторы
Проверено модерацией
Модерация
SocketTimeoutException в Vaadin при скачивании больших файлов