НейроАгент

Исправление SocketTimeoutException при загрузке больших файлов в Vaadin

Узнайте, как исправить SocketTimeoutException при загрузке больших файлов с помощью Vaadin. Найдите решения для проблем с таймаутом Firefox 30 секунд и ограничением Chromium в 1 ГБ. Полное руководство с примерами кода.

Вопрос

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 при загрузке больших файлов с помощью DownloadHandler Vaadin обусловлено сочетанием политик таймаута браузеров, механизмов обработки соединений и архитектуры потоковой передачи Vaadin. Агрессивный таймаут в 30 секунд в Firefox и ограничение в 1 ГБ в Chromium отражают разные подходы браузеров к обработке долгосрочных загрузок, в то время как SocketTimeoutException указывает на проблемы с соединением, которые сохраняются, несмотря на кажущуюся адекватность конфигураций таймаута.

Содержание

Понимание поведения таймаута браузеров

Непоследовательное поведение в разных браузерах можно объяснить принципиально разными подходами к обработке долгосрочных загрузок:

Таймаут в 30 секунд в Firefox

Firefox прерывает загрузки, которые кажутся зависшими после 30 секунд, показывая NS_BINDING_ABORTED в инспекторе сети. Это механизм безопасности Firefox, предотвращающий зависшие загрузки, которые могут указывать на проблемы с сервером или сетью. Согласно документации Mozilla, Firefox использует этот таймаут для предотвращения бесконечного ожидания, когда серверы не предоставляют правильные индикаторы прогресса.

Ограничение в 1 ГБ в Chromium

Браузеры на базе Chromium прекращают загрузку после примерно 1 ГБ данных из-за их стратегии управления буферами. Это ограничение существует потому, что браузеры выделяют буферы памяти для загрузок, и 1 ГБ представляет практический верхний предел, прежде чем проблемы управления памятью отменят операцию загрузки.


Основные причины проблем с таймаутом загрузки

Ограничения архитектуры DownloadHandler Vaadin

API DownloadHandler.fromInputStream в Vaadin 24.9.3 использует подход потоковой передачи, который создает несколько потенциальных узких мест:

  1. Синхронная обработка потока: Обработчик события загрузки обрабатывает входной поток синхронно, что может вызывать таймауты для очень больших файлов
  2. Зависимости от соединений Push: Как упоминается в документации Vaadin, соединения с долгим опросом (long-polling) могут быть прерваны прокси-серверами, что влияет на надежность загрузки
  3. Буферизация в памяти: Несмотря на потоковую передачу, реализация все еще может буферизовать данные в памяти перед отправкой в браузер

Управление соединениями на стороне сервера

java.net.SocketTimeoutException в org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite указывает на проблемы на уровне сокета:

java
// Переменная таймаута из getWriteTimeout() больше 30 секунд (по умолчанию 60)
// Но фактический таймаут происходит до достижения этого значения

Это указывает на то, что либо:

  • Таймаут не правильно настроен в конвейере соединения
  • Промежуточные прокси (например, nginx) применяют собственные политики таймаута
  • Реализация Tomcat NIO имеет внутренние механизмы таймаута

Вмешательство прокси-сервера Nginx

Несмотря на высокие значения настроек таймаута nginx (600 секунд), могут существовать другие директивы nginx, влияющие на загрузки:

nginx
# Эти настройки могут потребовать дополнительной конфигурации
proxy_buffering off;  # Отключить буферизацию для больших файлов
proxy_request_buffering off;
chunked_transfer_encoding on;

Решения и обходные пути

1. Реализация кодированной передачи фрагментами (Chunked Transfer Encoding)

Измените конфигурацию nginx для включения кодированной передачи фрагментами:

nginx
location /download/ {
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_set_header Transfer-Encoding chunked;
    proxy_set_header Connection "keep-alive";
    
    # Сохраните существующие настройки таймаута
    send_timeout 300;
    proxy_connect_timeout 600;
    proxy_send_timeout 600;
    proxy_read_timeout 600;
}

2. Использование StreamResource Vaadin с индикаторами прогресса

Вместо DownloadHandler.fromInputStream используйте StreamResource с правильным отслеживанием прогресса:

java
StreamResource resource = new StreamResource(() -> {
    try {
        return new FileInputStream(file);
    } catch (FileNotFoundException e) {
        throw new RuntimeException("Файл не найден", e);
    }
}, filename);

resource.setMIMEType(contentType);
resource.setCacheTime(0); // Отключить кэширование для загрузок

StreamRegistration registration = ui.getSession().getResourceRegistry().registerResource(resource);
String resourceUrl = registration.getResourceUri().toString();

3. Реализация потоковой передачи на стороне сервера с мониторингом прогресса записи

Создайте собственный обработчик загрузки с мониторингом прогресса:

java
DownloadHandler downloadHandler = DownloadHandler.fromInputStream(downloadEvent -> {
    try (InputStream inputStream = new FileInputStream(file);
         OutputStream outputStream = downloadEvent.getOutputStream()) {
        
        byte[] buffer = new byte[8192]; // Буфер 8 КБ
        int bytesRead;
        long totalBytesRead = 0;
        
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            totalBytesRead += bytesRead;
            
            // Периодически сбрасывать буфер для поддержания соединения
            if (totalBytesRead % (1024 * 1024) == 0) { // Сброс каждые 1 МБ
                outputStream.flush();
            }
        }
        
        return new DownloadResponse(outputStream, filename, contentType, file.length());
    } catch (IOException e) {
        throw new RuntimeException("Ошибка загрузки", e);
    }
});

4. Исправления, специфичные для браузеров

Для Firefox

Добавьте собственный заголовок для предотвращения преждевременного таймаута:

java
// В вашем обработчике загрузки
VaadinResponse response = downloadEvent.getResponse();
response.setHeader("X-Content-Transfer-Idle-Timeout", "300");

Для Chromium

Реализуйте функциональность возобновления с использованием заголовков HTTP Range:

java
// Проверьте, поддерживает ли браузер запросы с диапазоном
String rangeHeader = downloadEvent.getRequest().getHeader("Range");
if (rangeHeader != null) {
    // Разберите заголовок диапазона и отправьте частичное содержимое
    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
}

Лучшие практики для загрузки больших файлов

1. Использование выделенного сервлета для загрузки

Создайте отдельный сервлет для загрузки больших файлов, который обходит обычную обработку запросов Vaadin:

java
@WebServlet("/download/*")
public class LargeFileDownloadServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        String fileName = request.getPathInfo().substring(1);
        File file = new File("/path/to/files", fileName);
        
        if (file.exists()) {
            response.setContentType(getServletContext().getMimeType(fileName));
            response.setHeader("Content-Disposition", 
                "attachment; filename=\"" + fileName + "\"");
            response.setContentLength((int) file.length());
            
            Files.copy(file.toPath(), response.getOutputStream());
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}

2. Реализация асинхронной обработки

Для очень больших файлов реализуйте асинхронную обработку:

java
@Async
public CompletableFuture<Void> downloadLargeFile(File file, String filename, 
        String contentType, HttpServletResponse response) {
    try (InputStream inputStream = new FileInputStream(file);
         OutputStream outputStream = response.getOutputStream()) {
        
        byte[] buffer = new byte[16384]; // Буфер 16 КБ
        int bytesRead;
        
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            outputStream.flush();
        }
        
        return CompletableFuture.completedFuture(null);
    } catch (IOException e) {
        throw new CompletionException(e);
    }
}

3. Настройка пула соединений и таймаута

Оптимизируйте настройки пула соединений Tomcat:

yaml
server:
  tomcat:
    threads:
      max: 200
      min-spare: 10
    connection-timeout: 900000
    max-connections: 8192
    accept-count: 100
    max-http-post-size: 0 # Неограниченный размер POST
    max-swallow-size: 0 # Неограниченный размер тела запроса

Конфигурация, специфичная для браузеров

Конфигурация для Firefox

Добавьте эти заголовки для предотвращения преждевременного завершения:

java
response.setHeader("Connection", "keep-alive");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
response.setHeader("X-Content-Type-Options", "nosniff");

Конфигурация для Chromium

Для браузеров на базе Chromium реализуйте правильную поддержку заголовков Range:

java
// Проверьте наличие заголовка Range
String rangeHeader = request.getHeader("Range");
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
    String[] ranges = rangeHeader.substring(6).split("-");
    long start = Long.parseLong(ranges[0]);
    long end = ranges.length > 1 ? Long.parseLong(ranges[1]) : file.length() - 1;
    
    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
    response.setHeader("Content-Range", 
        "bytes " + start + "-" + end + "/" + file.length());
    response.setContentLength((int) (end - start + 1));
    
    // Перейдите к начальной позиции
    try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
        raf.seek(start);
        byte[] buffer = new byte[8192];
        long remaining = end - start + 1;
        
        while (remaining > 0) {
            int read = raf.read(buffer, 0, 
                (int) Math.min(buffer.length, remaining));
            outputStream.write(buffer, 0, read);
            remaining -= read;
        }
    }
}

Источники

  1. Документация Vaadin - Загрузки
  2. Vaadin RFC: Обработчики загрузки и выгрузки
  3. Сеть разработчиков Mozilla - Заголовки HTTP
  4. Конфигурация Push в Vaadin
  5. API обработки выгрузки Vaadin 24.8
  6. Справочник по конфигурации Tomcat
  7. Оптимизация Nginx для больших файлов

Заключение

Непоследовательное поведение SocketTimeoutException при загрузке больших файлов с помощью DownloadHandler Vaadin в первую очередь вызвано политиками таймаута, специфичными для браузеров, и различиями в управлении соединениями. 30-секундный таймаут безопасности Firefox и ограничение памяти в 1 ГБ в Chromium представляют принципиально разные подходы к обработке долгосрочных загрузок. Для решения этих проблем реализуйте следующие решения:

  1. Используйте кодированную передачу фрагментами в вашей конфигурации nginx для предотвращения проблем с буферизацией
  2. Реализуйте правильную обработку потоковых ресурсов с периодическим сбросом для поддержания соединений
  3. Создайте выделенные конечные точки загрузки, которые обходят обычную обработку запросов Vaadin для больших файлов
  4. Добавьте заголовки, специфичные для браузеров, и поддержку заголовков Range для обработки разных поведений браузеров
  5. Правильно настраивайте таймауты на стороне сервера как на уровне nginx, так и Tomcat

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