Бесконечная загрузка из-за Cloudflare

Мы перенесли наш сервер с одного провайдера VPS на другого и также обновили инстанс через launcher rebuild до последней версии — с 3.5.0.beta3 до 3.5.0.beta4.

Раньше инстанс стабильно работал за Cloudflare, но теперь при попытке доступа отображается бесконечная анимация загрузки с пятью точками.

В моём локальном файле hosts есть запись для обхода Cloudflare, поскольку у моего провайдера (Deutsche Telekom AG) ужасная политика пиринга, из-за чего доступ через Cloudflare иногда становится крайне медленным. Поэтому сначала я не заметил проблему, так как доступ без Cloudflare работал нормально. Затем я обновил инстанс и теперь не уверен, что стало причиной изменений: новый VPS или обновление Discourse. Я проверил через VPN и мобильную сеть, что проблема действительно в Cloudflare, а не в плохом пиринге моего провайдера, и другие пользователи сталкиваются с той же проблемой. И старый, и новый VPS поддерживают IPv6, а вся система полностью идентична, так как была перенесена как сырой образ.

Сообщений об ошибках нет ни в браузере (консоль), ни в прокси хост-системы, ни в Nginx внутри контейнера, ни в Rails или где-либо ещё. HTML-документы и несколько скриптов загружаются корректно, и при сравнении с теми, что отдаются при обходе Cloudflare, всё (что я проверил) идентично. Заголовки ответов тоже в основном совпадают, за исключением нескольких специфичных для Cloudflare, конечно. Последнее, что загружается, — это мини-профайлер:

Очистка кэша браузера, использование приватных окон и т. д. ничего не изменили. Также очистка/отключение кэша Cloudflare не помогло, значит, проблема не в кэше. Я временно полностью отключил кэширование CF для всего форума.

Стоит отметить, что форум работает на подпути за прокси Apache на хосте, следуя этим инструкциям: Serve Discourse from a subfolder (path prefix) instead of a subdomain
Ранее мы создавали просто символическую ссылку ln -s . forum вместо ссылок uploads/backups и дважды дублировали правила переписывания из инструкций, что работало годами (и сейчас тоже без Cloudflare). Но в рамках отладки я переключился на указанные инструкции, чтобы убедиться, что внутренний прокси применяет все правила корректно. Доверенный заголовок — CF-Connecting-IP, хотя я также включил cloudflare.template.yml, даже если это немного дублирует настройки. Я также пробовал менять различные части этих шаблонов и вышеупомянутых инструкций туда-сюда, пытаясь проверить, влияют ли заголовки IP прокси, так как отсутствие CF-Connecting-IP является одной из проблем при обходе Cloudflare.

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

Надеюсь, кто-то подскажет, как отладить это, или знает, было ли изменение между 3.5.0.beta3 и 3.5.0.beta4, которое могло быть связано с проблемой. Думаю, откат назад может быть проблематичным?

Вот наш инстанс: https://dietpi.com/forum/
РЕДАКТИРОВАНИЕ: Я временно отключил Cloudflare. Но есть CNAME, который всё ещё проходит через Cloudflare, так что их можно сравнить: https://www.dietpi.com/forum/

Интересная проблема.

Просто https://www.dietpi.com/forum/ висит вечно.

$ wget https://www.dietpi.com/forum/
--2025-05-03 10:52:18--  https://www.dietpi.com/forum/
Resolving www.dietpi.com (www.dietpi.com)... 104.21.12.65, 172.67.193.183, 2606:4700:3035::6815:c41, ...
Connecting to www.dietpi.com (www.dietpi.com)|104.21.12.65|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: âindex.html.1â

    [<=>                            

Интересно, что запросы вроде https://www.dietpi.com/forum/site.json выполняются успешно.

https://www.dietpi.com/forum/t/why-there-are-two-kernals-in-my-raspberry-pi4/23355 не работает и висит вечно, но
https://www.dietpi.com/forum/t/why-there-are-two-kernals-in-my-raspberry-pi4/23355.json работает.

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

Эта последняя строка при загрузке через Cloudflare:

      <discourse-assets-json>
        <div class="hidden" id="data-preloaded" data-preloaded="&quot;topic_list&quot;:&quot;{\&quot;users\&quot;...\&quot;:false,\&quot;allowed_iframes\&quot;:\&quot;https://dietpi.com/forum/discobot/certificate.svg\

Мне пришлось её укоротить, так как она значительно превышает лимит символов для сообщения. Обычно документ продолжается следующим образом:

      <discourse-assets-json>
        <div class="hidden" id="data-preloaded" data-preloaded="&quot;topic_list&quot;:&quot;{\&quot;users\&quot;...\&quot;:false,\&quot;allowed_iframes\&quot;:\&quot;https://dietpi.com/forum/discobot/certificate.svg\&quot;,\&quot;can_permanently_delete...

Другие страницы также зависают в точно таком же месте. Кажется, мы на верном пути.

EDIT: Ах, подождите, я проверил неправильно — другие страницы зависают в других местах. Значит, дело не в этом конкретном HTML-элементе или атрибуте.

Да, каждая страница/HTML-документ при загрузке через браузер снова и снова зависает на одном и том же символе (в том числе в режиме инкогнито и т. д.). Но другая страница зависает в другом месте. И при загрузке через curl они также всегда зависают в одном и том же месте, но другом, а wget — опять же всегда в одном и том же месте, но немного отличном от предыдущих. Очень странно.

У вас включена какая-либо оптимизация?

Нет, никаких оптимизаций (content). У меня была включена функция 103 Early Hints, но я уже отключил её в попытке решить проблему. Попробовал то же самое с настройками протокола, но это тоже ничего не изменило:

Кстати, в заголовке ответа отсутствует content-length. Не может ли это вызвать проблему? Я имею в виду, что его нет и при обходе Cloudflare, но, возможно, тогда проблема именно в Cloudflare? РЕДАКТИРОВАНИЕ: Похоже, это нормально для динамических страниц, то же самое с нашими страницами на WordPress и Matomo, которые, однако, не вызывают никаких проблем.

И ещё одно наблюдение при работе с curl. Вывод в STDOUT приводит к отображению полного HTML-документа, но соединение всё равно зависает, и в конце:

  <p class='powered-by-link'>Powered by <a href="https://www.discourse.org">Discourse</a>, best viewed with JavaScript enabled</p>
</footer>



  </body>

</html>

Но при попытке сохранить его через -o или простую перенаправку, или даже просто передав в grep, зависание происходит в другом месте:

            <div class="link-bottom-line">
                <a href='/forum/c/general-discussion/7' class='badge-wrapper bullet'>
                  <span class='badge-category-bg' style='background-color: #F7941D'></span>
                  <span class='badge-category clear-badge'>
                    <span class='category-name'>General Discussion</span>
                  </span>
                </a>

И я на 100% могу воспроизвести эти самые 73728 байт, обращаясь к https://www.dietpi.com/forum/ через curl, просто выводя результат в консоль. Это так странно :face_with_monocle:.


Итак:

  • Все клиенты зависают при загрузке любого HTML-документа из нашего экземпляра Discourse.
  • Каждый клиент зависает на одном и том же байте при загрузке одной и той же страницы.
  • Разные клиенты зависают в разных точках, но при повторении с тем же клиентом зависание происходит на одном и том же байте.
  • Каждая страница зависает в разной точке документа и при разном размере загруженных данных.
  • Один и тот же инструмент curl зависает в разных точках при выводе в STDOUT по сравнению с передачей в канал или сохранением документа где-либо.
  • wget может загрузить полный документ (по крайней мере https://www.dietpi.com/forum/) в файл, но всё равно зависает в конце; то же самое происходит, когда curl https://www.dietpi.com/forum/ выводит полный документ в консоль, но зависает в конце.

Думаю, это может быть связано с буферизацией. Но при расследовании я заметил кое-что ещё.

wget -O - https://www.dietpi.com/forum/latest

Заканчивается так:

  </body>
</html>

Но соединение никогда не закрывается.

Теория: где-то есть проблема конфигурации, например, несоответствие версий HTTP или заголовков (например, keep-alive соединение), и это становится проблемой только тогда, когда документ больше X (я подозреваю, 64 КБ).

Да, wget всегда загружает документ целиком, а curl делает то же самое при выводе прямо в консоль, но соединение не закрывается. То же самое происходит с гораздо меньшими документами: я тестировал документ объемом 14 КБ по теме с всего двумя постами. Но даже меньшие файлы обычно не загружаются полностью через curl при перенаправлении вывода или сохранении в файл, то же самое наблюдается и в браузере.

Оба инструмента всегда показывают HTTP/2, и в Cloudflare у меня включены исходящие запросы к источнику по HTTP/2. Однако стоит явно протестировать использование других версий HTTP. Вчера я отключил все настройки протокола в Cloudflare, показанные на скриншоте выше, но это не помогло. Но я попробую ещё раз. Также я могу включить логи доступа на сервере, чтобы увидеть фактический входящий запрос от Cloudflare.

Я перепробовал все комбинации поддерживаемых версий HTTP (1.1–3) и TLS (1.2–1.3), но это не дало никакого эффекта. Также я отключил поддержку HTTP/3, и запросы к источнику HTTP/2 снова инициируют возобновление соединения с 0-RTT. Разницы нет: curl продолжает зависать ровно на 73 728 байтах при загрузке https://www.dietpi.com/forum/.

Что касается теории о слишком большом размере документа, то страница https://www.dietpi.com/dietpi-software.html имеет размер 199 475 байт и загружается без проблем. Стоит отметить, что сервер (тот же веб-сервер) размещает статический сайт, экземпляр MkDocs, WordPress и Matomo, которые все работают безупречно. Также там есть экземпляр Grafana, где фронтенд-веб-сервер выступает в роли прокси через UNIX-сокет.

Однако я согласен, что проблема, похоже, связана с буферами, размерами чанков или чем-то подобным. Просто странно, что размер скачанного данных до зависания так сильно варьируется между клиентами и страницами, при этом он остаётся абсолютно одинаковым, несмотря на изменение версий протокола, и соединение даже не закрывается после полной загрузки документа. Будто отсутствует сигнал остановки, хотя на данном этапе мне не хватает знаний о HTTP. Поэтому я подумал о заголовке content-length, но он, очевидно, не является обязательным.

Веб-сервер также выступает в роли прокси для контейнера Discourse через UNIX-сокет. Я мог бы включить TCP-прослушивание, чтобы сделать экземпляр Discourse дополнительно доступным без прокси (оставив Nginx внутри контейнера, разумеется).

Можете попробовать KeepAlive Off в Apache?

Думаю, это хотя бы частично позволит исключить веб-сервер из списка возможных причин, так что попробовать стоит.

Никаких изменений. Также из документации Apache:

Кроме того, соединение Keep-Alive с клиентом HTTP/1.0 возможно только в том случае, если длина контента известна заранее.

Таким образом, поскольку отсутствует заголовок content-length, вероятно, имеет смысл, что он в любом случае не используется для этого запроса.

Поскольку это требует пересборки, я сделаю это немного позже, когда активность нашего общего веб-сайта будет минимальной. Э-э, я просто думаю о HTTPS… Похоже, мне нужно внести некоторые кастомные изменения во внутреннюю конфигурацию Nginx, чтобы UNIX-сокет оставался функциональным, как и обычные HTTP-соединения, при прослушивании дополнительного порта для HTTPS с TLS-сертификатами хоста, но без перенаправления или принуждения к использованию HTTPS. … Также был бы интересен дополнительный TCP-порт для обычного HTTP для клиентов, которые могут игнорировать HSTS.

Не используете ли вы случайно RocketLoader в CloudFlare? Я знаю, что с некоторыми другими скриптами это вызывает проблемы. Также вы очистили кэш CF? Используете ли вы входящие правила в CF, которые могли быть привязаны к вашему старому IP-адресу VPS и не были обновлены на новый?

Без RocketLoader: Обратите внимание, что, согласно приведённым выше тестам с curl и wget, которые не интерпретируют никакой синтаксис и, следовательно, не загружают JavaScript, стили или что-либо ещё, проблема заключается в том, что загрузка исходного HTML-документа всегда зависает.

Кэш Cloudflare для форума не активен; в любом случае исходные HTML-документы никогда не кэшировались.

Специфичных правил для VPS нет. В целом правил для форума тоже нет, за исключением тех, что предназначены для обхода кэша. Проблема проявляется в обоих случаях, поэтому кэш также не является причиной.

Во время тестирования обхода прокси Apache2 на хосте контейнера Discourse и отключения принудительных перенаправлений HTTPS в Cloudflare для проверки обычных HTTP-соединений через curl я наконец обнаружил причину проблемы в Cloudflare:

Не уверен, что именно изменилось при переходе на наш новый VPS и/или при обновлении Discourse с версии 3.5.0.beta3 до 3.5.0.beta4, или это совпадение, но похоже, что что-то в HTML, CSS или JavaScript-документах Discourse вызывает сбой функции перенаправления HTTPS в Cloudflare для встроенных URL. Похоже, что частичные и зависшие запросы curl действительно не связаны с этим, или, возможно, всё же связаны. Странно, что во вкладке сети браузера можно увидеть частичное содержимое HTML-документа, как будто функция перенаправления HTTPS обрабатывает его по мере потоковой передачи документа.

Возможно, у кого-то ещё есть экземпляр и аккаунт Cloudflare, чтобы протестировать это и выяснить, является ли это общей проблемой или связана ли она конкретно с нашей настройкой?

Кстати, для тестирования обхода прокси, а также работы по HTTP, сохраняя при этом активное соединение через прокси, ручная настройка конфигурации Nginx внутри контейнера следующим образом работает безупречно:

root@dietpi-discourse:/var/www/discourse# cat /etc/nginx/conf.d/outlets/server/10-http.conf
listen unix:/shared/nginx.http.sock;
set_real_ip_from unix:;
listen 8080;
listen [::]:8080;
listen 8443 ssl;
listen [::]:8443 ssl;
http2 on;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

ssl_certificate /shared/fullchain.cer;
ssl_certificate_key /shared/dietpi.com.key;

ssl_session_tickets off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:1m;

Конечно, важно отключить перенаправления HTTPS и заголовок HSTS, а также открыть добавленные порты.

И ещё одна находка: мы используем mod_sed, чтобы добавлять код трекера Matomo ко всем ответам text/html прямо перед закрывающим тегом </head>. Отключение этого модуля для Discourse (или обход прокси Apache2) также решает проблему, несмотря на активные автоматические переписывания HTTPS в Cloudflare. Отключение любого из этих двух элементов решает проблему. На всех остальных страницах комбинация работает отлично, в том числе на очень больших страницах, которые больше, чем проблемные страницы форума. Возможно, два фильтра — сначала mod_set на нашем прокси, а затем встроенные переписывания URL от Cloudflare — вызывают какую-то поломку, связанную с размером документа или чанков или чем-то ещё.

Сейчас мы внедряем трекер через редактирование темы Discourse, и я дополнительно отключил автоматические переписывания HTTPS в Cloudflare. На всём нашем сайте нет смешанного контента. И если он есть, лучше увидеть и исправить это, чем позволять Cloudflare скрывать это навсегда.

Я почти уверен, что это не сработает.

Не совсем понимаю, какую проблему вы пытаетесь решить, но, вероятно, вам нужно включить параметр force_https в вашем файле app.yml.

Предполагаю, что само название «Cloudflare Automatic HTTPS Rewrites» может ввести в заблуждение. У Cloudflare есть две функции:

  • «Always Use HTTPS» перенаправляет все обычные HTTP-запросы на HTTPS, точно так же, как это делает force_https в Discourse. Обе функции ранее были включены, и я отключил обе, чтобы проверить, связана ли проблема с HTTPS или с бесконечной загрузкой страниц Discourse и зависанием запросов curl. Это сработало отлично, даже полностью устранив проблему для HTTPS-запросов, но только потому, что я одновременно отключил «Cloudflare Automatic HTTPS Rewrites».
  • «Cloudflare Automatic HTTPS Rewrites» изменяет HTML-, CSS- и JavaScript-документы, заменяя все встроенные обычные HTTP-URL-адреса на их HTTPS-варианты, если Cloudflare считает, что хост доступен через HTTPS (на основе списка предварительной загрузки HSTS и т.п.). Это делается во избежание предупреждений о смешанном содержимом.

Принудительное использование HTTPS или его отсутствие на уровне Cloudflare, прокси-сервера хоста или Discourse не имеет значения. Причиной проблемы стала комбинация фильтра mod_sed на прокси-сервере хоста и встроенных правок обычных HTTP-адресов со стороны Cloudflare. То есть контент документов проходил через фильтр на двух этапах. Проблема заключалась не в том, что происходило фактическое изменение содержимого (на нашем сайте нет смешанного содержимого, поэтому «Cloudflare Automatic HTTPS Rewrites» на самом деле не меняет тело документа), а, вероятно, была связана с фрагментами, буфером или таймингом.