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
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 при скачивании больших файлов
- Почему поведение отличается в Firefox и Chromium
- Проблемы Tomcat NIO и connection-timeout
- Настройки nginx для избежания таймаутов при проксировании
- Кастомный DownloadHandler с чанкингом и flush
- Дополнительные рекомендации по Vaadin Spring Boot
- Источники
- Заключение
Причины 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
server:
tomcat:
max-http-post-size: -1 # или 10GB
connection-timeout: 900000
max-connections: 10000
Но для стриминга WAR на standalone — в server.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 для больших файлов! Делайте кастом:
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:
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:
<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 не для этого.
Источники
- Easyupload multi upload triggers SocketTimeoutException on reading big file - Vaadin Forum
- Spring boot java.net.SocketTimeoutException while uploading large files - Stack Overflow
- SocketTimeoutException while downloading large file and this behavior is inconsistent among different browsers - Stack Overflow
- Downloads stop after 1GB depending of network – nginx
- 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. Ваши файлы полетят без сбоев, а пользователи скажут спасибо за скорость.