НейроАгент

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

Решение проблемы SocketTimeoutException при загрузке больших файлов с использованием Vaadin и Spring-Boot. Узнайте о браузерных исправлениях для таймаутов Firefox и Chrome при передаче больших файлов.

Вопрос

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? Требуются ли дополнительные конфигурации или модификации кода для поддержки загрузки больших файлов согласованно в разных браузерах?

НейроАгент

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

Содержание

Понимание основных причин

SocketTimeoutException в сценарии загрузки больших файлов является результатом множества взаимосвязанных факторов:

Ограничения таймаутов Tomcat

Трейс стека указывает на NioEndpoint$NioSocketWrapper.doWrite() как на источник, что свидетельствует о том, что сам Tomcat прерывает соединение. Хотя вы установили connection-timeout: 900000, это влияет только на первоначальное установление соединения, а не на таймаут при непрерывной передаче данных. Согласно исследованиям Stack Overflow, таймаут соединения в Tomcat контролирует, как долго Tomcat будет ждать следующий запрос на постоянном соединении, но не предотвращает таймауты во время потоковой передачи данных.

Сложности на уровне прокси

Ваша конфигурация прокси nginx с таймаутами в 600 секунд может по-прежнему быть недостаточной для передачи больших файлов, особенно учитывая:

  • Сбой Firefox в 30 секунд: Это соответствует многим значениям по умолчанию для прокси в отношении постоянства соединений
  • Ограничение Chromium в 1 ГБ: Указывает на проблемы управления буферами, при которых прокси не может поддерживать состояние соединения в течение длительного времени

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

Реализация DownloadHandler в Vaadin

Ваша текущая реализация с использованием DownloadHandler.fromInputStream с прямыми потоками ввода файлов может быть неоптимальной для больших файлов. Документация Vaadin suggests that for large files, you should consider streaming approaches rather than loading the entire file into memory or using direct file input streams that can cause buffer saturation.


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

NS_BINDING_ABORTED в Firefox после 30 секунд

Последовательный сбой Firefox через 30 секунд указывает на проблему постоянства соединения:

  • Keep-Alive прокси: Firefox может закрывать соединения быстрее, чем другие браузеры
  • Управление пулом соединений: Firefox может неэффективно повторно использовать соединения
  • Функции безопасности: Усиленные настройки безопасности Firefox могут прерывать медленные передачи

Ошибка NS_BINDING_ABORTED указывает на то, что Firefox проактивно прерывает то, что он воспринимает как зависшие соединения. Исследования показывают, что это распространено, когда политики безопасности браузера интерпретируют медленные загрузки как потенциальные угрозы безопасности.

Сбой браузера Chromium при ~1 ГБ

Сбои браузеров на базе Chromium после передачи примерно 1 ГБ указывают на проблемы управления буферами и памятью:

  • Ограничения памяти: Браузеры имеют ограничения на объем данных, которые они будут буферизировать перед принудительной очисткой
  • Размер TCP окна: После передачи ~1 ГБ масштабирование TCP окна может вызывать проблемы
  • Цикл событий JavaScript: Клиентские компоненты Vaadin могут иметь насыщение очереди событий

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


Решения по конфигурации

Комплексная конфигурация Tomcat

Добавьте эти свойства в ваш файл application.properties:

properties
# Таймауты коннекторов Tomcat
server.connection-timeout=1800000
server.tomcat.connection-timeout=1800000
server.tomcat.async-timeout=1800000
server.tomcat.keep-alive-timeout=1800000

# Конфигурация multipart в Spring Boot
spring.servlet.multipart.max-file-size=2GB
spring.servlet.multipart.max-request-size=2GB
spring.http.multipart.max-file-size=2GB
spring.http.multipart.max-request-size=2GB

Как отмечено в многих ответах на Stack Overflow, эти значения таймаута должны быть значительно выше ожидаемого времени загрузки.

Улучшенная конфигурация Nginx

Обновите вашу конфигурацию nginx следующим образом:

nginx
send_timeout 3600;
proxy_connect_timeout 600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 2G;

Директивы proxy_buffering off и proxy_request_buffering off критически важны для загрузки больших файлов, поскольку они предотвращают буферизацию nginx всего ответа, что может вызывать проблемы с памятью и таймауты.

Соображения по памяти JVM

Убедитесь, что ваша JVM имеет достаточно памяти для операций с большими файлами:

bash
-Xms2g -Xmx4g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC

Модификации кода для загрузки больших файлов

Оптимизированная реализация DownloadHandler

Вместо использования прямых потоков ввода файлов реализуйте подход потоковой передачи:

java
hiddenDownloadAnchor.setHrefAndDownload(DownloadHandler.fromInputStream(
    downloadEvent -> {
        BufferedInputStream inputStream = new BufferedInputStream(
            new FileInputStream(file), 8192);
        return new DownloadResponse(
            inputStream,
            name,
            type,
            length) {
                @Override
                public void writeResponse(OutputStream outputStream) throws IOException {
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    long totalBytesWritten = 0;
                    
                    try (BufferedOutputStream bufferedOutput = new BufferedOutputStream(outputStream)) {
                        while ((bytesRead = inputStream.read(buffer)) != -1) {
                            bufferedOutput.write(buffer, 0, bytesRead);
                            totalBytesWritten += bytesRead;
                            
                            // Обновление прогресса UI (при необходимости)
                            UI.getCurrent().access(() -> {
                                // Обновление индикатора прогресса
                            });
                        }
                        bufferedOutput.flush();
                    }
                }
            };
    }));

Альтернатива: Streaming ResponseBody Spring Boot

Для лучшего контроля рассмотрите использование StreamingResponseBody Spring Boot напрямую:

java
@GetMapping("/download/{fileId}")
public ResponseEntity<StreamingResponseBody> downloadFile(@PathVariable String fileId) {
    File file = getFile(fileId);
    
    StreamingResponseBody responseBody = outputStream -> {
        try (InputStream inputStream = new FileInputStream(file)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        }
    };
    
    return ResponseEntity.ok()
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .header(HttpHeaders.CONTENT_DISPOSITION, 
                "attachment; filename=\"" + file.getName() + "\"")
        .body(responseBody);
}

Соображения по миграции на Vaadin 24.8+

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


Мониторинг и отладка

Мониторинг соединений

Добавьте мониторинг для отслеживания состояний соединений:

java
@Component
public class DownloadMonitor implements ServletContextListener {
    private static final Logger logger = LoggerFactory.getLogger(DownloadMonitor.class);
    
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        sce.getServletContext().addListener(new HttpSessionListener() {
            @Override
            public void sessionCreated(HttpSessionEvent se) {
                logger.info("Сессия создана: {}", se.getSession().getId());
            }
            
            @Override
            public void sessionDestroyed(HttpSessionEvent se) {
                logger.info("Сессия уничтожена: {}", se.getSession().getId());
            }
        });
    }
}

Отладка, специфичная для браузеров

Для решения проблем, специфичных для браузеров, реализуйте:

  • Firefox: Временно отключите функции безопасности, чтобы проверить, вызывают ли они таймаут в 30 секунд
  • Chromium: Проверьте консоль браузера на наличие сетевых ошибок в точке передачи 1 ГБ
  • Анализ сети: Используйте инструменты разработчика браузера для мониторинга постоянства соединений и состояний буфера

Ведение журналов на стороне сервера

Улучшите ведение журналов Tomcat для захвата деталей таймаутов:

properties
logging.level.org.apache.catalina=DEBUG
logging.level.org.apache.tomcat.util.net=DEBUG

Это предоставит подробную информацию о состояниях соединений и событиях таймаута, как описано в документации Apache Tomcat.


Заключение

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

  1. Исправления конфигурации: Реализуйте комплексные настройки таймаутов на уровнях Tomcat, Spring Boot и прокси nginx
  2. Оптимизация кода: Замените прямые потоки ввода файлов на буферизованные реализации потоковой передачи
  3. Совместимость с браузерами: Учитывайте различия в обработке соединений, специфичные для браузеров
  4. Мониторинг: Добавьте подробное ведение журналов и мониторинг для выявления шаблонов таймаутов

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

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