Интеграции ломаются не там, где менял код

Интеграции ломаются не там, где менял код

-- У нас ЭДО не работает. С утра.

-- Что-нибудь обновляли?

-- Нет. То есть... вчера обновили криптосервис. Но там только патч безопасности, мы в код вообще не лезли.

-- А в коде и не надо было лезть.

Этот диалог случился в пятницу вечером. Что характерно -- он мог случиться в любой пятницу вечером, потому что интеграции имеют дурную привычку ломаться именно тогда, когда никто ничего не менял. Точнее -- когда все уверены, что никто ничего не менял.

За 17 лет работы с 1С я усвоил одну вещь про интеграции: они ломаются не в коде. Они ломаются на стыках -- между системами, между версиями, между ожидаемым и реальным. И чем больше у вас интеграций, тем чаще пятничный вечер будет начинаться с сообщения "не работает".

Порт 9998 больше не отвечает

Компания использует электронный документооборот. Подписание документов -- через локальный криптосервис. Это отдельная программа, которая висит в трее на сервере, слушает определённый порт и принимает запросы на подписание. 1С отправляет HTTP-запрос на localhost:9998, криптосервис подписывает документ и возвращает результат.

Работало полгода без единого сбоя. А потом перестало.

Первая реакция специалиста на месте -- перезапустить сервис. Перезапустил. Не помогло. Перезагрузил сервер. Не помогло. Проверил, что сервис запущен -- запущен. Проверил, что иконка в трее есть -- есть. Проверил, что лицензия активна -- активна.

Мне переслали скриншот ошибки: "Не удалось установить соединение с localhost:9998". Классика. Порт не отвечает.

Ошибка соединения с криптосервисом на порту 9998 в журнале 1С

Подключился удалённо. Первое, что сделал -- netstat -an | findstr 9998. Пусто. Порт 9998 никто не слушает. Сервис запущен, но порт пуст.

Второй заход: netstat -an | findstr LISTEN -- посмотреть, какие порты вообще открыты. И вот он, знакомый процесс криптосервиса. Только слушает он теперь порт 9999.

Вчерашний "патч безопасности" оказался минорным обновлением, которое в числе прочего сменило порт по умолчанию. Не в документации, не в changelog -- в конфигурационном файле. Новая версия при установке перезаписала конфиг, и порт тихо поменялся с 9998 на 9999.

В коде 1С порт был прописан константой:

Соединение = Новый HTTPСоединение("localhost", 9998);

Поменял 9998 на 9999. ЭДО заработало. Чинка заняла пять минут, но диагностика до моего подключения -- два часа. Потому что все искали проблему в 1С. В коде. В настройках ЭДО. В сертификатах. Везде, кроме того места, где она реально была -- в конфигурации внешнего сервиса.

Конечно, правильнее было бы хранить порт не в коде, а в настройках:

ПортКриптосервиса = Константы.ПортКриптосервиса.Получить();
Соединение = Новый HTTPСоединение("localhost", ПортКриптосервиса);

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

Файл, которого нет на диске

Вторая история -- из того же проекта, но про другой слой ЭДО. Обработка входящих электронных документов.

Входящие документы ЭДО попадают в 1С из двух источников. Первый -- прямой обмен с оператором ЭДО через API. Файлы приходят по сети и сохраняются в базе 1С как двоичные данные в реквизите типа ХранилищеЗначения. Второй источник -- импорт из файловой системы. Бухгалтер скачивает документы с портала оператора, кладёт в папку, запускает загрузку. Файлы остаются на диске, а в базе хранится только путь.

Код обработки один и тот же. Берёт "файл", разбирает XML, извлекает реквизиты, создаёт документ в 1С. Проблема в слове "берёт".

Функция ПолучитьДанныеФайлаЭДО(ФайлЭДО)
    
    Если ФайлЭДО.ХранилищеДанных <> Неопределено Тогда
        // Файл в базе — читаем из хранилища
        Возврат ФайлЭДО.ХранилищеДанных.Получить();
    КонецЕсли;
    
    // Файл на диске — читаем по пути
    Возврат Новый ДвоичныеДанные(ФайлЭДО.ПутьКФайлу);
    
КонецФункции

Выглядит логично. Если есть данные в хранилище -- берём оттуда. Если нет -- читаем с диска по пути. Работает в обоих сценариях.

До тех пор, пока сервер не переехал.

Схема хранения файлов ЭДО: двоичные данные в базе и пути к файлам на диске

При миграции на новый сервер базу перенесли. Файлы, которые лежали в базе как двоичные данные, -- переехали вместе с ней. А файлы на диске? Путь D:\EDO\Входящие\2025\ на новом сервере не существует. Диск D -- другого размера, папки другие, структура другая.

Документы, загруженные через API, продолжали обрабатываться. Документы, импортированные с диска, -- нет. Ошибка: "Файл не найден". Только для части документов. Случайной, на первый взгляд, части.

Диагностика заняла время, потому что ошибка была непредсказуемой. Один документ обрабатывается, следующий -- нет. Зависело не от типа документа, не от контрагента, не от даты. Зависело от того, каким способом этот конкретный файл попал в систему -- через API или через импорт из папки.

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

Заодно переписал функцию получения данных, добавив проверку доступности:

Функция ПолучитьДанныеФайлаЭДО(ФайлЭДО)
    
    // Приоритет: данные в базе
    Если ФайлЭДО.ХранилищеДанных <> Неопределено Тогда
        ДвоичныеДанные = ФайлЭДО.ХранилищеДанных.Получить();
        Если ДвоичныеДанные <> Неопределено Тогда
            Возврат ДвоичныеДанные;
        КонецЕсли;
    КонецЕсли;
    
    // Файл на диске
    Если ЗначениеЗаполнено(ФайлЭДО.ПутьКФайлу) Тогда
        Файл = Новый Файл(ФайлЭДО.ПутьКФайлу);
        Если Файл.Существует() Тогда
            Возврат Новый ДвоичныеДанные(ФайлЭДО.ПутьКФайлу);
        Иначе
            ЗаписьЖурналаРегистрации("ЭДО.ФайлНеДоступен",
                УровеньЖурналаРегистрации.Предупреждение,,,
                "Файл не найден: " + ФайлЭДО.ПутьКФайлу);
        КонецЕсли;
    КонецЕсли;
    
    Возврат Неопределено;
    
КонецФункции

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

Похожую историю с молчаливыми сбоями я описывал в разборе бага с API БСП -- там стандартный метод тоже возвращал пустоту без ошибок, потому что кастомный справочник нарушил неявный контракт.

Где искать, если код не менялся

Обе истории объединяет одно: код 1С не менялся. Баг не в коде. Баг в окружении. И это -- самый коварный тип проблем с интеграциями, потому что первый инстинкт любого разработчика -- лезть в код. Открыть конфигуратор. Поставить точку останова. Пройтись отладчиком. А отлаживать нечего -- код работает ровно так, как написан.

Вот чек-лист, который я использую, когда интеграция "вдруг" перестала работать:

1. Сеть и порты. netstat, telnet, ping. Порт слушается? Хост доступен? Firewall не блокирует? Я видел случай, когда обновление Windows добавило правило в брандмауэр, и порт, который работал год, оказался закрыт. Без предупреждений, без логов -- просто timeout при соединении.

2. Версии внешних сервисов. Что обновлялось за последние 48 часов? Не только на сервере 1С -- на всех серверах, участвующих в обмене. Веб-сервер, криптосервис, оператор ЭДО, маркетплейс. Минорные обновления любят менять дефолтные настройки. Про подводные камни интеграции с маркетплейсами у меня есть отдельный разбор на примере Wildberries.

3. Пути к файлам. Все ли папки доступны? Все ли пути валидны? Сервер не переезжал? Сетевой диск не отвалился? Права на папки не изменились? Любимая история: бэкап-скрипт, который чистил "временные" файлы, удалил папку обмена.

4. Сертификаты. SSL-сертификат не истёк? Клиентский сертификат для взаимной аутентификации обновлён? Корневой сертификат удостоверяющего центра установлен на новом сервере? Сертификаты -- чемпион по внезапным отказам. Работало -- раз -- не работает. Потому что сертификат выдаётся на год, и через год он истекает. Внезапно. Посреди рабочего дня.

5. Формат данных. Контрагент не поменял формат XML? Оператор ЭДО не обновил схему? API маркетплейса не добавил обязательное поле? Внешние системы меняются без предупреждения. Точнее -- предупреждение было, в рассылке, которую никто не читает.

Чек-лист диагностики: сеть, версии, пути, сертификаты, формат данных

6. Кодировка и локаль. Новый сервер -- другая локаль? UTF-8 вместо Windows-1251? Десятичный разделитель точка вместо запятой? Формат даты MM/DD/YYYY вместо DD.MM.YYYY? Я видел, как обмен данными с внешней системой ломался из-за того, что на новом сервере стояла английская Windows. Числа форматировались с точкой, внешняя система ожидала запятую. Документы создавались с суммами в десять раз больше реальных.

7. Логи. Не только журнал регистрации 1С. Логи внешнего сервиса. Логи веб-сервера. Логи операционной системы -- Application и System в EventViewer. Часто внешний сервис пишет в свой лог подробное объяснение, почему он отклонил запрос, а 1С получает только лаконичное "ошибка".

8. Таймауты. Запрос уходит, но ответ не приходит вовремя. Нагрузка на внешний сервис выросла, время ответа увеличилось с 2 до 15 секунд, а таймаут в 1С -- 10 секунд. Раньше укладывались, теперь нет. Код не менялся. Внешний сервис формально работает. Но интеграция падает по timeout. Настройка мониторинга через Prometheus и Grafana помогает отслеживать такие деградации заранее, до того как пользователи начнут жаловаться.

Этот чек-лист -- не исчерпывающий. Но он покрывает 80% случаев, с которыми я сталкивался. И главное -- он заставляет смотреть за пределы кода 1С.

Интеграция -- это договор, а не код

Когда я пишу интеграционный модуль, я пишу код. Вызов HTTP, разбор ответа, маппинг данных, обработка ошибок. Это занимает 80% времени. Но причина 80% сбоев -- не в этом коде.

Интеграция -- это договор между двумя системами. Как любой договор, он имеет множество неявных условий. Порт 9998. Папка D:\EDO\Входящие. Формат даты ГГГГ-ММ-ДД. Кодировка UTF-8. Сертификат действителен до 15.06.2026. Таймаут ответа не более 10 секунд.

Код фиксирует лишь часть этих условий. Остальное -- подразумевается. И именно подразумеваемое ломается первым.

Вот что я делаю, чтобы минимизировать сюрпризы:

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

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

Добавляю проверки доступности. Перед первым обращением к внешнему сервису -- проверяю, что он отвечает. Порт открыт? Ответ приходит? Формат ответа ожидаемый? Если нет -- понятное сообщение об ошибке, а не "Ошибка при вызове метода контекста" через три уровня вложенности вызовов.

Логирую всё на границе. Каждый исходящий запрос. Каждый входящий ответ. URL, порт, заголовки, тело запроса, код ответа, тело ответа. Время отправки, время получения, длительность. В production это генерирует много данных, но при сбое -- экономит часы. Вместо "не работает" вы видите: отправлен запрос на localhost:9998, получен TCP RST. Порт не слушается. Всё ясно за секунду.

Делаю health check. Простой регламентный скрипт, который раз в 5 минут проверяет доступность всех внешних сервисов. Порт отвечает? Сертификат не истекает в ближайшие 30 дней? Папка обмена существует и доступна для записи? Если что-то не так -- уведомление в Telegram. Проблема видна до того, как позвонит бухгалтер.

Архитектура интеграции: health check, логирование, настройки в константах

Интеграция -- это не "написал код и забыл". Это живой процесс. Внешние системы обновляются. Серверы переезжают. Сертификаты истекают. Сети перенастраиваются. Каждое из этих событий может сломать интеграцию, не затронув ни строчки вашего кода.

И если подходить к интеграции как к коду -- вы будете каждый раз искать баг в конфигураторе, тратя часы на отладку рабочего кода. А если подходить как к договору -- вы начнёте с проверки условий этого договора. Порт. Путь. Сертификат. Формат. Версия. Пять минут на чек-лист, и вы либо найдёте причину, либо с уверенностью скажете: "Проблема не на нашей стороне".

Тот криптосервис с портом 9999 -- мне потом рассказали, что в changelog обновления была строчка: "Changed default port from 9998 to 9999 to avoid conflict with monitoring agent". Строчка была. Её никто не прочитал. Потому что "это же просто патч безопасности, мы в код не лезли".

Правильно. В код не лезли. А надо было лезть в changelog.