Веб

SocketTimeoutException в Vaadin: таймауты при скачивании

Решение SocketTimeoutException при загрузке больших файлов в Vaadin с Spring Boot и Tomcat. Настройка nginx (proxy_max_temp_file_size 0), Tomcat soTimeout и Vaadin DownloadHandler с Content-Length для Firefox и Chromium.

SocketTimeoutException при загрузке больших файлов с Vaadin и Spring-Boot: Несогласованное поведение в разных браузерах

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

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

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

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

Попытки конфигурации

Конфигурация Nginx

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

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

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

Реализация кода

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

Наблюдения

  1. Проблема возникает только при загрузке файлов, а не при загрузке (которые работают нормально)
  2. Длина содержимого установлена правильно с соответствующими MIME типами
  3. Firefox последовательно прерывает загрузку через 30 секунд независимо от размера файла
  4. Браузеры на базе Chromium прерывают загрузку после скачивания примерно 1ГБ
  5. В стеке трейса исключение исходит из NioSocketWrapper.doWrite(NioEndpoint.java:1410)
  6. Значение таймаута записи в режиме отладки больше 30 секунд (по умолчанию 60)

Стек трейса

org.apache.catalina.connector.ClientAbortException: java.net.SocketTimeoutException
 at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:342)
 ...
Caused by: java.net.SocketTimeoutException
 at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1410)
 ...

Вопрос

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

SocketTimeoutException при скачивании больших файлов в Vaadin с Spring Boot обычно связан с буферизацией nginx (proxy_max_temp_file_size по умолчанию 1 ГБ) и таймаутами записи в Tomcat NIO (NioSocketWrapper.doWrite). В Firefox это NS_BINDING_ABORTED через 30 секунд — клиент просто не ждет дольше, а в Chromium обрыв после 1 ГБ происходит из-за остановки прокси до отправки буфера. Решение: отключите proxy_buffering в nginx, поднимите proxy_max_temp_file_size до 0 (без лимита), используйте стриминг в Vaadin DownloadHandler с явным Content-Length и мониторьте логи Tomcat на ClientAbortException.


Содержание


Причины SocketTimeoutException в Vaadin

Представьте: вы запускаете скачивание 2-ГБ файла через Vaadin, и вдруг — бац! — SocketTimeoutException в логах Tomcat. Почему так? Всё упирается в цепочку: клиент → nginx → Tomcat → Vaadin.

Сначала nginx. По умолчанию proxy_max_temp_file_size = 1 ГБ. Когда ответ от Tomcat превышает этот лимит, прокси перестает читать бэкенд, пока не вышлет буфер клиенту. Клиент (особенно медленный) не успевает — и таймаут. Официальный тикет nginx описывает это классически: “Nginx stops reading from the backend until all disk-buffered data are sent”.

Дальше Tomcat NIO. Стектрейс на NioEndpoint.java:1410 — это doWrite, где сокет ждет записи. Если клиент “завис” (буфер полон), кидается SocketTimeoutException, заворачиваясь в ClientAbortException. Ваши connection-timeout 900000 мс (15 мин) — хорошо, но socket-level таймаут (soTimeout) может быть меньше.

Vaadin добавляет масла в огонь: DownloadHandler.fromInputStream буферизует поток, если не настроить стриминг правильно. Без Content-Length браузеры нервничают, особенно Firefox.

А теперь вопрос: почему только при скачивании, а не загрузке? Потому что upload — это клиент шлет серверу (быстро), download — сервер льет клиенту (медленно, зависит от сети).


Поведение в разных браузерах

Firefox vs Chromium — это не баг, а фича. В Firefox 30-секундный лимит на stalled requests (NS_BINDING_ABORTED) жесткий: если нет прогресса, браузер абортит. StackOverflow кейс подтверждает: Firefox режет через ~30с, Chromium жрет до 1 ГБ, потом таймаут.

Почему Chromium терпеливее? Они лучше обрабатывают chunked transfer-encoding, но упираются в nginx-буфер. Если сеть медленная (Debian за прокси — типично), 1 ГБ уходит 5-10 мин, и proxy_send_timeout срабатывает.

Тестировали на Brave/Chrome? То же самое. Решение не в браузере, а в сервере: сделайте передачу chunked или с прогрессом, чтобы клиент видел активность.

Коротко: Firefox — нетерпеливый клиент, Chromium — терпит до буфера прокси.


Настройка Nginx против nginx таймаут

Ваша текущая конфигурация (proxy_send_timeout 600 и т.д.) — шаг в сторону, но не то. Главный виновник — буферизация. Добавьте в location /:

proxy_buffering off;
proxy_max_temp_file_size 0; # без лимита диска
proxy_request_buffering off; # для симметрии
proxy_send_timeout 1800s; # 30 мин
proxy_read_timeout 1800s;
send_timeout 1800s;
tcp_nodelay on; # минимизирует задержки

Почему 0? Nginx перестанет дампить на диск и будет стримить напрямую. StackOverflow пример решил проблему именно так — лимит был 1024M.

Перезагрузите nginx и тестите. Логи: tail -f /var/log/nginx/error.log — ищите “upstream timed out”.

Ещё трюк: если WAR за прокси, убедитесь, что X-Forwarded-For и т.д. не мешают.


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

В application.yml уже connection-timeout: 900000 — ок, но добавьте:

yaml
server:
 tomcat:
 connection-timeout: 900000
 max-http-post-size: 0 # без лимита
 max-http-form-post-size: 0
 socket:
 soTimeout: 900000 # критичный для NIO write
 receive-buffer-size: 65536
 send-buffer-size: 65536
 threads:
 max: 200 # под нагрузку

В Tomcat server.xml (если не embedded):

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
 connectionTimeout="900000"
 socket.appReadBufSize="65536"
 socket.appWriteBufSize="65536"
 soTimeout="900000" />

Tomcat mailing list упоминает: NioSocketWrapper.doWrite таймаутит именно на soTimeout. Ваши 60с по умолчанию — мало для 1+ ГБ.

Tомкат исходники показывают: doWrite блокируется, если сокет не готов.

Пересоберите WAR, деплоите — и Firefox должен протянуть дольше.


Корректировка кода Vaadin для больших файлов

Код почти верный, но доработайте для стриминга. Vaadin docs рекомендуют:

java
hiddenDownloadAnchor.setHref(DownloadHandler.fromInputStream(
 downloadEvent -> {
 try {
 FileInputStream fis = new FileInputStream(file);
 return new DownloadResponse(
 fis, // стрим, не байты!
 name,
 type,
 file.length() // Content-Length обязателен!
 ).onProgress((readBytes, contentLength) -> {
 // Лог прогресса, если нужно
 getLogger().info("Downloaded: " + readBytes + "/" + contentLength);
 }).whenComplete((response, error) -> {
 if (error != null) {
 getLogger().error("Download failed", error);
 }
 });
 } catch (Exception e) {
 throw new RuntimeException(e);
 }
 },
 DownloadHandler.DownloadHandlerOptions.forInputStream()
 .withContentLength(file.length()) // явно!
));

Ключ: Content-Length заранее (file.length()). Без него — chunked, Firefox бесится. Добавьте onProgress — браузеры видят активность.

Vaadin forum подтверждает: стрим vs буфер решает.

Для Vaadin 24.9.3 это сработает. Тестите на малом файле сначала.


Диагностика java.net.SocketTimeoutException

Не гадать — копать. Шаги:

  1. Логи: Tomcat catalina.out — grep “SocketTimeoutException|ClientAbortException”. Nginx access/error — время ответа >600s?

  2. Browser devtools: Network tab. Firefox: stalled? Chromium: (failed) net::ERR_CONNECTION_RESET после 1ГБ.

  3. tcpdump: tcpdump -i any -w dump.pcap port 443 or port 80 во время скачивания. Wireshark: retransmits? Zero window?

  4. curl тест: curl -o file.bin --limit-rate 1M -v http://yourserver/download — симулирует медленного клиента.

  5. strace nginx: strace -p $(pgrep nginx) — висит на write()?

GitHub Vaadin issue советует логировать lifecycle в handler.

Если ничего — проблема в сети/firewall. Проверьте MTU, прокси upstream.

С этим вы найдете корень за 15 мин.


Источники

  1. StackOverflow: SocketTimeoutException while downloading large file
  2. Vaadin Docs: Downloads
  3. Nginx Trac: Downloads stop after 1GB
  4. StackOverflow: Nginx download stops after 1GB
  5. Vaadin Forum: SocketTimeoutException on big file
  6. Vaadin GitHub: Upload/download handlers
  7. Tomcat Mailing List: SocketTimeoutException downloading large files
  8. Tomcat Source: NioEndpoint.java

Заключение

SocketTimeoutException в Vaadin уйдет, если отключить буферизацию nginx (proxy_max_temp_file_size 0, proxy_buffering off), поднять soTimeout в Tomcat до 900000 мс и добавить Content-Length с onProgress в DownloadHandler. Firefox перестанет абортить через 30с, Chromium — после 1 ГБ. Тестируйте поэтапно: nginx → Tomcat → код. Если логи чистые — смотрите сеть. Это стандартный пайплайн для больших файлов, и он работает стабильно во всех браузерах. Удачи с деплоем!

Авторы
Проверено модерацией
Модерация
SocketTimeoutException в Vaadin: таймауты при скачивании