Странная проблема с кодировкой на странице категорий

Я пытаюсь найти странную проблему в установке без Docker (я понимаю, что поддержка такого типа установки ограничена или отсутствует, поэтому ищу лишь подсказки, что может быть не так — наша внутренняя команда по сборке использовала инструкции для «разработчиков», чтобы понять, как собрать необходимые пакеты). Я смог подтвердить, что проблема специфична для нашего способа установки: моя команда инфраструктуры не хочет использовать установку в Docker (они предпочитают собирать всё самостоятельно), поэтому я запускаю изолированные среды как в Docker, так и без него, с копией нашей базы данных, чтобы определить, где возникает проблема, и это определённо артефакт нашего способа настройки.

При обновлении с версии 3.3.2 до 3.3.3 некоторые сотрудники нашего форума, не являющиеся носителями английского языка, заметили, что текст раздела «О нас» для секций, использующих акцентированные символы, кодируется некорректно:

Интересно, что заголовок, как и весь остальной текст, кодируется правильно. Фактически, само сообщение, используемое для раздела «О нас», также закодировано верно:

Я подтвердил, что это тот же самый текст, отредактировав его и увидев изменения на странице категорий.

Таким образом, проблема заключается в чём-то специфичном для отображения этого текста на странице категорий.

Просматривая document.characterSet в моём браузере, я вижу, что он правильно определяется как UTF-8. База данных также показывает формат UTF-8.

Мне интересно, сможет ли кто-нибудь подсказать, чем отличается отображение этого текста на странице категорий. Я предполагаю, что это какой-то Ruby-пакет, который собран неправильно (возможно, без поддержки UTF-8) и используется при отображении этого текста, но не другого текста в системе, либо что-то, что обрабатывает текст сообщения «О нас» и обрезает его (что, как я заметил, происходит здесь; однако у нас также есть ссылка на внешний французский форум с сообщением, которое не обрезается, но я предполагаю, что оно всё равно обрабатывается тем же кодом).

Спасибо за любые подсказки. Я немного в тупике.

Я вижу, что иногда всё работает правильно:

При получении сырого файла categories.json видно, что ошибка есть только в excerpt:

        "description": "Esta sección del Foro se dedica a las personas usuarias de openSUSE que forman parte de la comunidad lingüística castellana, de tal forma que dichas personas puedan consultar y participar en el foro en dicha lengua (sea el dialecto español o cualquiera de las variedades latinoamericanas, etc.).",
        "description_text": "Esta sección del Foro se dedica a las personas usuarias de openSUSE que forman parte de la comunidad lingüística castellana, de tal forma que dichas personas puedan consultar y participar en el foro en dicha lengua (sea el dialecto español o cualquiera de las variedades latinoamericanas, etc.).",
        "description_excerpt": "Esta sección del Foro se dedica a las personas usuarias de openSUSE que forman parte de la comunidad lingüística castellana, de tal forma que dichas personas puedan consultar y participar en el foro en dicha lengua (sea el dialecto español o cualquiera de las variedades latinoamericanas, etc.).",

Создание той же категории на try.discourse.org и проверка categories.json даёт правильный результат:

        "description": "Esta sección del Foro se dedica a las personas usuarias de openSUSE que forman parte de la comunidad lingüística castellana, de tal forma que dichas personas puedan consultar y participar en el foro en dicha lengua (sea el dialecto español o cualquiera de las variedades latinoamericanas, etc.).",
        "description_text": "Esta sección del Foro se dedica a las personas usuarias de openSUSE que forman parte de la comunidad lingüística castellana, de tal forma que dichas personas puedan consultar y participar en el foro en dicha lengua (sea el dialecto español o cualquiera de las variedades latinoamericanas, etc.).",
        "description_excerpt": "Esta sección del Foro se dedica a las personas usuarias de openSUSE que forman parte de la comunidad lingüística castellana, de tal forma que dichas personas puedan consultar y participar en el foro en dicha lengua (sea el dialecto español o cualquiera de las variedades latinoamericanas, etc.).",

Не уверен, какой будет следующий шаг в отладке этой проблемы на вашей установке, но, возможно, стоит сосредоточиться на коде, который генерирует excerpt, а также учесть, что проблема возникла из-за интерпретации кодировки UTF-8 как iso-8859-1.

Да, я так и предполагаю — скорее всего, именно там, где формируется отрывок. Просто не уверен, где именно это находится в самом коде. Но знание того, что нужно искать термин «excerpt», определённо помогает — спасибо!

Мне показалось, что в какой-то момент данные передавались в кодировке iso-8859-1, поэтому спасибо и за это подтверждение (я не был на 100% уверен, что именно это неверное кодирование я наблюдаю, но казалось, что всё верно).

То, что вы видели на try.discourse.org, я тоже наблюдал в своей установке в Docker (ну, по крайней мере, итоговый результат с правильной кодировкой :slight_smile: )

Спасибо!

Вы можете легко проверить это с помощью:

○ → ipython3

In [1]: 'Этот раздел Форота посвящён пользователям openSUSE, которые являются частью испаноязычного сообщества, таким образом, чтобы эти пользователи могли обращаться и участвовать в форуме на этом языке (будь то испанский диалект или любая из латиноамериканских разновидностей и т. д.)'.encode('utf-8').decode('iso-8859-1')
Out[1]: 'Esta sección del Foro se dedica a las personas usuarias de openSUSE que forman parte de la comunidad lingüÃ\xadstica castellana, de tal forma que dichas personas puedan consultar y participar en el foro en dicha lengua (sea el dialecto español o cualquiera de las variedades latinoamericanas, etc.).'

Мне кажется, что здесь задействовано какое-то кэширование. Это не очень помогает, но именно на это я бы обратил внимание.

Если только они просто ненавидят Docker, они могут собрать свои собственные образы с помощью discourse_docker. Тогда они смогут точно увидеть, что происходит, и не будут вынуждены доверять образам других людей.

Круто, спасибо за это.

Я так и думал, но изменение сообщения привело к обновлению, поэтому я не думаю, что это кэширование — проблема в неправильном кодировании.

Я предложил несколько вариантов, но в итоге команда инфраструктуры решила использовать пакеты, собранные с помощью сервиса сборки. Я не думаю, что это связано с «ненавистью к Docker» (хотя, вероятно, они предпочли бы использовать podman), а скорее с тем, чтобы использовать инструменты управления конфигурацией, которыми они управляют всем, единым образом. Наличие отдельного решения, использующего Docker/podman, добавит сложности в настройку CI/CD, которую они используют (по крайней мере, насколько я понимаю).

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

Я понимаю, почему они считают, что подход Discourse безумен, и они действительно хотят, чтобы всё управлялось в рамках единой системы.

Но. Последний клиент, который настаивал на использовании своих любимых инструментов, в итоге заплатил мне почти 20 часов работы, чтобы получить рабочую резервную копию для переноса на хостинг discourse.org. Предыдущий клиент заплатил ещё больше, чтобы настроить свою кастомную сборку и предотвратить падения сайта несколько раз в неделю, а через год снова заплатил мне за перенос на хостинг discourse.org. :slight_smile:

Удачи!

Спасибо за совет. Хорошая новость в том, что резервные копии из нашей продакшн-системы отлично работают в Docker-окружении (я это проверил), так что если/когда они решат, что использование установки на базе Docker — правильный путь, мы будем в отличной форме. У нас большой объём данных (перенесённых несколько лет назад с vBulletin на Discourse), и в целом всё работает довольно хорошо, с редкими небольшими сбоями.

В результате мы многому научились в том, как работает Discourse, так что в целом это не так уж плохо. :slight_smile:

Похоже, что /categories.json — это API-эндпоинт, а не статический файл, который создаётся и затем читается, поэтому, думаю, это помогает сузить проблему либо до Ruby, либо до JavaScript. Я нашёл схему для этого эндпоинта, но не очень хорошо знаком с Ruby (за годы я накопил большой опыт работы с различными языками программирования, поэтому чтение кода на большинстве языков для меня не проблема, даже если я не умею на них писать — я довольно легко улавливаю суть). Похоже, что JavaScript выполняется в основном в браузере, а Ruby — на сервере (хотя я заметил, что установлен также Node.js, так что это обобщение может быть не совсем верным).

Если мне удастся найти функцию, которая обрабатывает /categories (поскольку .json в конце, похоже, просто указывает коду, как форматировать вывод; я наблюдаю подобное поведение, например, в /top и /top.rss), это должно помочь сузить область поиска в коде, и тогда станет ясно, какие Ruby-гемы (я почти уверен, что это будет код на Ruby) нужно проверить на правильность сборки.

Кажется, это что-то специфичное для функций извлечения фрагментов — я только что заметил, что это происходит и на наших страницах результатов поиска:

(например)

Текст следующий:

Я не вижу кнопку «Поделиться».

Это я процитировал в ответе пользователю (panorain). Я наткнулся на это случайно: при попытке посмотреть свою активность я получаю ошибку сервера 500, а вывод из /logs показывает ошибку выполнения «input string cannot be empty» в lib/excerpt_parser.rb.

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

В моей установке на базе Docker я могу просмотреть свою активность без ошибок; странно, однако, что база данных в этой установке восстановлена из недавней резервной копии продакшн-сервера, где эта проблема существует.

Похоже, мы обновили nokogiri до версии 1.17.2, а в Docker-образе установлена версия 1.16.7. Я подозреваю, что именно это стало причиной проблемы. Буду пытаться откатить это обновление (а также любые другие изменения, внесённые одновременно с ним).

Итак, я снова понизил версию нашего пакета до nokogiri 1.16. Что меня смущает. Каждый раз, когда я обновляю gem для уменьшения дублирования пакетов, я проверяю, были ли в main соответствующие изменения — их не было. Если только я что-то упустил.

        "description": "Witaj w polskiej sekcji społeczności openSUSE!",
        "description_text": "Witaj w polskiej sekcji społeczności openSUSE!",
        "description_excerpt": "Witaj w polskiej sekcji społeczności openSUSE!",

Как видите, у нас правильный текст дважды, и ломается он только при прогоне через PrettyText.excerpt. Как это обрабатывается в main?

@hendersj Я уже готовлю пакет main, чтобы мы могли протестировать его с копией базы данных.

Думаю, это было обработано в DEV: Update nokogiri to 1.18.1 (#30554) · discourse/discourse@affe26f · GitHub

Но мне интересно… в lib/retrieve_title.rb

doc = Nokogiri.HTML5(html, encoding:)

разве это не должно быть:

doc = Nokogiri.HTML5(html, encoding: Encoding::UTF_8)

@pfaffman Этот странный код также присутствует в релизе 3.4.0. :slight_smile:

Не могли бы вы проверить, действительно ли его следует вызывать с пустым параметром encoding?

Есть какие-то причины, по которым вы так считаете? UTF-8 используется по умолчанию.

[1] pry(main)> Nokogiri::VERSION
=> "1.18.2"

[2] pry(main)> t = '<div>Witaj w polskiej sekcji społeczności openSUSE!</div>'
=> "<div>Witaj w polskiej sekcji społeczności openSUSE!</div>"

[3] pry(main)> Nokogiri.HTML5(t).to_s
=> "<html><head></head><body><div>Witaj w polskiej sekcji społeczności openSUSE!</div></body></html>"

[4] pry(main)> Nokogiri.HTML5(t, encoding: Encoding::UTF_8).to_s
=> "<html><head></head><body><div>Witaj w polskiej sekcji społeczności openSUSE!</div></body></html>"

[5] pry(main)> Nokogiri.HTML5(t).to_s == Nokogiri.HTML5(t, encoding: Encoding::UTF_8).to_s
=> true

Функция retrieve_title используется для извлечения заголовков с внешних URL (например, YouTube), и, хотя я не очень хорошо знаком с этим путем выполнения кода, было бы удивительно, если бы она стала источником вашей проблемы.

Если вы делаете что-то другое (например, используете эту функцию в собственном плагине), то параметр кодировки там берётся из заголовка content-type полученного ресурса:

        if !encoding && content_type = _response["content-type"]&.strip&.downcase
          if content_type =~ /charset="?([a-z0-9_-]+)"?/
            encoding = Regexp.last_match(1)
            encoding = nil if !Encoding.list.map(&:name).map(&:downcase).include?(encoding)
          end
        end

        max_size = max_chunk_size(uri) * 1024
        title = extract_title(current, encoding)

Поэтому можно предположить, что ответный веб-сервер сообщает неверный content-type.

Потому что во всех остальных вызовах в этом патче указаны параметры encoding:

Только в retrieve title их нет. Это кажется несогласованным. И именно некорректная обработка кодировки UTF-8 стала предметом обсуждения, которое привело к созданию этой ветки.

Ага:

это сокращение для:

doc = Nokogiri.HTML5(html, encoding: encoding)

принудительное использование UTF8 там нарушит парсинг ответов от веб-серверов, которые не в UTF8

Спасибо за уточнение. Возвращаемся к упаковке 3.4.0.