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)
Реализация кода
hiddenDownloadAnchor.setHrefAndDownload(DownloadHandler.fromInputStream(
downloadEvent -> new DownloadResponse(
new FileInputStream(file),
name,
type,
length)));
Наблюдения
- Проблема возникает только при загрузке файлов, а не при загрузке (которые работают нормально)
- Длина содержимого установлена правильно с соответствующими MIME типами
- Firefox последовательно прерывает загрузку через 30 секунд независимо от размера файла
- Браузеры на базе Chromium прерывают загрузку после скачивания примерно 1ГБ
- В стеке трейса исключение исходит из
NioSocketWrapper.doWrite(NioEndpoint.java:1410) - Значение таймаута записи в режиме отладки больше 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
- Поведение в разных браузерах
- Настройка Nginx против nginx таймаут
- Конфигурация Tomcat и Spring Boot
- Корректировка кода Vaadin для больших файлов
- Диагностика java.net.SocketTimeoutException
- Источники
- Заключение
Причины 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 — ок, но добавьте:
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 рекомендуют:
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
Не гадать — копать. Шаги:
-
Логи: Tomcat catalina.out — grep “SocketTimeoutException|ClientAbortException”. Nginx access/error — время ответа >600s?
-
Browser devtools: Network tab. Firefox: stalled? Chromium: (failed) net::ERR_CONNECTION_RESET после 1ГБ.
-
tcpdump:
tcpdump -i any -w dump.pcap port 443 or port 80во время скачивания. Wireshark: retransmits? Zero window? -
curl тест:
curl -o file.bin --limit-rate 1M -v http://yourserver/download— симулирует медленного клиента. -
strace nginx:
strace -p $(pgrep nginx)— висит на write()?
GitHub Vaadin issue советует логировать lifecycle в handler.
Если ничего — проблема в сети/firewall. Проверьте MTU, прокси upstream.
С этим вы найдете корень за 15 мин.
Источники
- StackOverflow: SocketTimeoutException while downloading large file
- Vaadin Docs: Downloads
- Nginx Trac: Downloads stop after 1GB
- StackOverflow: Nginx download stops after 1GB
- Vaadin Forum: SocketTimeoutException on big file
- Vaadin GitHub: Upload/download handlers
- Tomcat Mailing List: SocketTimeoutException downloading large files
- 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 → код. Если логи чистые — смотрите сеть. Это стандартный пайплайн для больших файлов, и он работает стабильно во всех браузерах. Удачи с деплоем!