framework/skills/bsl-practices/integration-patterns/SKILL.md
Для 1C HTTP/REST/SOAP, auth, retry, webhooks
npx skillsauth add steelmorgan/1c-agent-based-dev-framework integration-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ключевой принцип: Контракт определяется до кода. HTTP-сервис — тонкий слой разбора запроса и формирования ответа; бизнес-логика живёт в общих модулях. Секреты — только через ОбщегоНазначения.*ДанныеВБезопасноеХранилище*, никогда в коде или константах.
Перед написанием кода зафиксируйте контракт в виде структуры или комментария:
Изменение контракта без версионирования — нарушение совместимости. Добавляйте новые поля, не удаляя старые. Смену семантики существующих полей оформляйте через новую версию (/v2/…).
Обработчик HTTP-сервиса должен делать только три вещи: разобрать запрос, вызвать бизнес-функцию, вернуть ответ. Не кладите бизнес-логику прямо в обработчик.
// Обработчик метода POST ресурса /orders
Функция ОбработатьПОСТ(Запрос)
// 1. Разбор тела
ТелоСтрокой = Запрос.ПолучитьТелоКакСтроку();
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(ТелоСтрокой);
ПараметрыЗаказа = ПрочитатьJSON(ЧтениеJSON, Тип("Структура"));
// 2. Валидация обязательных полей
Если НЕ ЗначениеЗаполнено(ПараметрыЗаказа.НомерВнешнего) Тогда
Возврат ОтветОшибки(400, "VALIDATION_ERROR",
НСтр("ru = 'Поле НомерВнешнего обязательно'"));
КонецЕсли;
// 3. Бизнес-вызов
Попытка
РезультатСозданияЗаказа = ИнтеграцияЗаказов.СоздатьЗаказ(ПараметрыЗаказа);
Исключение
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ИнтеграцияЗаказов.СоздатьЗаказ'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
Возврат ОтветОшибки(500, "INTERNAL_ERROR",
НСтр("ru = 'Внутренняя ошибка сервера'"));
КонецПопытки;
// 4. Формирование ответа
Возврат ОтветУспеха(201, РезультатСозданияЗаказа);
КонецФункции
// Вспомогательные функции формирования ответа
Функция ОтветОшибки(КодСостояния, КодОшибки, Сообщение)
Ответ = Новый HTTPСервисОтвет(КодСостояния);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
СтруктураОшибки = Новый Структура("error, message", КодОшибки, Сообщение);
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, СтруктураОшибки);
Ответ.УстановитьТелоИзСтроки(ЗаписьJSON.Закрыть());
Возврат Ответ;
КонецФункции
Функция ОтветУспеха(КодСостояния, Данные)
Ответ = Новый HTTPСервисОтвет(КодСостояния);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, Данные);
Ответ.УстановитьТелоИзСтроки(ЗаписьJSON.Закрыть());
Возврат Ответ;
КонецФункции
Все секреты (токены, пароли, ключи) хранятся исключительно через обёртки ОбщегоНазначения.ЗаписатьДанныеВБезопасноеХранилище / ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища (регистр сведений БезопасноеХранилищеДанных). Никогда — в константах конфигурации, параметрах сеанса, реквизитах справочников или текстах модулей.
Подробно по каждой схеме — см. ../security/references/auth.md.
// Получение учётных данных из безопасного хранилища
УстановитьПривилегированныйРежим(Истина);
УчётныеДанные = ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища("ИнтеграцияСВнешнимСервисом", "Логин,Пароль");
УстановитьПривилегированныйРежим(Ложь);
Логин = УчётныеДанные.Логин;
Пароль = УчётныеДанные.Пароль;
// Не логируйте: Логин/Пароль, Заголовок Authorization, тело с персданными
Соединение = Новый HTTPСоединение(
"api.example.com",
, // порт — по умолчанию 443 для HTTPS
Логин,
Пароль,
, // прокси
30, // таймаут (сек)
Новый ЗащищённоеСоединение); // OpenSSL / CertificateAuth — см. ниже
// Токен из безопасного хранилища
УстановитьПривилегированныйРежим(Истина);
ТокенДоступа = ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища("ИнтеграцияСВнешнимСервисом", "ТокенДоступа");
УстановитьПривилегированныйРежим(Ложь);
Запрос = Новый HTTPЗапрос("/api/v1/resource");
Запрос.Заголовки.Вставить("Authorization", "Bearer " + ТокенДоступа);
Запрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
// Путь к сертификату и пароль — из безопасного хранилища
УстановитьПривилегированныйРежим(Истина);
ПараметрыCertAuth = ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища("ИнтеграцияСертификат", "ПутьКСертификату,Пароль");
УстановитьПривилегированныйРежим(Ложь);
СертификатКлиента = Новый СертификатКлиентаФайл(
ПараметрыCertAuth.ПутьКСертификату,
ПараметрыCertAuth.Пароль);
ЗащИтоеСоединение = Новый ЗащищённоеСоединение(
, , СертификатКлиента, ,
Истина); // проверять серверный сертификат
Соединение = Новый HTTPСоединение(
"api.example.com", , , , , 30, ЗащищённоеСоединение);
Внешний вызов ненадёжен. Всегда оборачивайте в Попытка/Исключение. Для изменяющих операций используйте идемпотентный ключ и защиту от повторного выполнения.
Функция ВызватьВнешнийAPIСПовтором(URLПуть, ТелоЗапросаJSON, КлючИдемпотентности = "")
МаксимумПопыток = 3;
ОжиданиеМеждуПопытками = 2; // секунды
Для НомерПопытки = 1 По МаксимумПопыток Цикл
Попытка
Соединение = Новый HTTPСоединение(
"api.example.com", , , , , 30,
Новый ЗащищённоеСоединение);
Запрос = Новый HTTPЗапрос(URLПуть);
Запрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
// Ключ идемпотентности — безопасное повторение без дублирования
Если ЗначениеЗаполнено(КлючИдемпотентности) Тогда
Запрос.Заголовки.Вставить("Idempotency-Key", КлючИдемпотентности);
КонецЕсли;
Запрос.УстановитьТелоИзСтроки(ТелоЗапросаJSON, КодировкаТекста.UTF8);
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
Если Ответ.КодСостояния >= 200 И Ответ.КодСостояния < 300 Тогда
Возврат Ответ.ПолучитьТелоКакСтроку();
ИначеЕсли Ответ.КодСостояния >= 400 И Ответ.КодСостояния < 500 Тогда
// Клиентская ошибка — не ретраить
ВызватьИсключение СтрШаблон(
НСтр("ru = 'Ошибка запроса (HTTP %1). Повтор нецелесообразен.'"),
Ответ.КодСостояния);
ИначеЕсли Ответ.КодСостояния >= 500 Тогда
// Серверная ошибка — ретраим
ВызватьИсключение СтрШаблон(
НСтр("ru = 'Сервер вернул %1'"), Ответ.КодСостояния);
КонецЕсли;
Исключение
ИнфоОшибки = ИнформацияОбОшибке();
УровеньЖР = ?(НомерПопытки < МаксимумПопыток,
УровеньЖурналаРегистрации.Предупреждение,
УровеньЖурналаРегистрации.Ошибка);
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ВнешняяИнтеграция.HTTPЗапрос'"), УровеньЖР, , ,
СтрШаблон(НСтр("ru = 'Попытка %1/%2. Ошибка: %3'"),
НомерПопытки, МаксимумПопыток,
ПодробноеПредставлениеОшибки(ИнфоОшибки)));
Если НомерПопытки = МаксимумПопыток Тогда
ВызватьИсключение;
КонецЕсли;
// Пауза перед следующей попыткой
ТекущаяДата = ТекущаяДата();
Пока ТекущаяДата() < ТекущаяДата + ОжиданиеМеждуПопытками Цикл
КонецЦикла;
КонецПопытки;
КонецЦикла;
Возврат "";
КонецФункции
Изменяющие операции (POST создание, изменение состояния) должны быть защищены от двойного выполнения.
Паттерны идемпотентности:
Idempotency-Key в заголовке (UUID, сформированный на стороне клиента);НомерВнешнего) в теле с уникальным индексом на стороне 1С;// Проверка дубля перед записью (серверная идемпотентность)
Функция НайтиЗаказПоВнешнемуНомеру(НомерВнешнего)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Заказы.Ссылка КАК Ссылка
|ИЗ
| Документ.ЗаказКлиента КАК Заказы
|ГДЕ
| Заказы.НомерВнешнего = &НомерВнешнего";
Запрос.УстановитьПараметр("НомерВнешнего", НомерВнешнего);
Выборка = Запрос.Выполнить().Выбрать();
Возврат ?(Выборка.Следующий(), Выборка.Ссылка, Неопределено);
КонецФункции
// В обработчике POST:
СуществующийЗаказ = НайтиЗаказПоВнешнемуНомеру(ПараметрыЗаказа.НомерВнешнего);
Если СуществующийЗаказ <> Неопределено Тогда
// Дубль — вернуть 200 с данными существующего заказа (не 201)
Возврат ОтветУспеха(200, ПолучитьДанныеЗаказа(СуществующийЗаказ));
КонецЕсли;
Ответ об ошибке должен быть предсказуемым. Клиент должен уметь отличить:
| HTTP-код | Тип ошибки | Поведение клиента | |----------|-----------|------------------| | 400 | Ошибка валидации входных данных | Не ретраить; исправить запрос | | 401 | Ошибка аутентификации | Обновить токен; не ретраить немедленно | | 409 | Конфликт / дубль | Не ретраить; обработать дубль | | 422 | Бизнес-ошибка (данные корректны, но нарушено правило) | Не ретраить; показать пользователю | | 500 | Внутренняя ошибка | Ретраить с backoff | | 503 | Сервис временно недоступен | Ретраить с backoff |
// Единая структура тела ошибки
// {"error": "VALIDATION_ERROR", "message": "...", "correlationId": "..."}
Функция ОтветОшибкиС Корреляцией(КодСостояния, КодОшибки, Сообщение, КорреляцияИд)
Ответ = Новый HTTPСервисОтвет(КодСостояния);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.Заголовки.Вставить("X-Correlation-Id", КорреляцияИд);
ТелоОшибки = Новый Структура;
ТелоОшибки.Вставить("error", КодОшибки);
ТелоОшибки.Вставить("message", Сообщение);
ТелоОшибки.Вставить("correlationId", КорреляцияИд);
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ТелоОшибки);
Ответ.УстановитьТелоИзСтроки(ЗаписьJSON.Закрыть());
Возврат Ответ;
КонецФункции
Никогда не возвращайте стек вызовов 1С, имена модулей или внутренние ID объектов метаданных во внешний API.
Для работы с SOAP-сервисами используйте WSПрокси, создаваемый через WSОпределения. Аутентификацию передавайте через параметры прокси, не в теле сообщения.
// Создание WSПрокси с аутентификацией
Функция СоздатьПроксиПлатёжногоШлюза()
УстановитьПривилегированныйРежим(Истина);
УчётныеДанные = ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища("ПлатёжныйШлюзSOAP", "Логин,Пароль");
УстановитьПривилегированныйРежим(Ложь);
WSОпределения = Новый WSОпределения(
"https://payment.example.com/service?wsdl",
УчётныеДанные.Логин,
УчётныеДанные.Пароль,
, // прокси
30); // таймаут
Прокси = WSОпределения.СоздатьWSПрокси("PaymentService", "PaymentPort");
Прокси.Пользователь = УчётныеДанные.Логин;
Прокси.Пароль = УчётныеДанные.Пароль;
Прокси.Таймаут = 30;
Возврат Прокси;
КонецФункции
// Вызов с обработкой ошибок
Попытка
ПроксиWS = СоздатьПроксиПлатёжногоШлюза();
// XDTO-объект для тела запроса
ФабрикаXDTO = ПроксиWS.ФабрикаXDTO;
ЗапросXDTO = ФабрикаXDTO.Создать(ФабрикаXDTO.Тип("http://payment.example.com/", "PayRequest"));
ЗапросXDTO.Amount = СуммаПлатежа;
ЗапросXDTO.OrderId = Строка(ИдентификаторЗаказа);
ЗапросXDTO.Currency = "RUB";
ОтветXDTO = ПроксиWS.Pay(ЗапросXDTO);
Если ОтветXDTO.Status <> "OK" Тогда
ВызватьИсключение СтрШаблон(НСтр("ru = 'Платёжный шлюз вернул: %1'"), ОтветXDTO.Status);
КонецЕсли;
Исключение
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ПлатёжныйШлюз.Оплата'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
correlationId, requestId, X-Correlation-Id);Authorization (Bearer-токен, Basic-credentials);ОбщегоНазначения.*ДанныеВБезопасноеХранилище*);// Корреляция через запрос
КорреляцияИд = Запрос.Заголовки.Получить("X-Correlation-Id");
Если НЕ ЗначениеЗаполнено(КорреляцияИд) Тогда
КорреляцияИд = Строка(Новый УникальныйИдентификатор);
КонецЕсли;
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ИнтеграцияЗаказов.ПолучитьЗаказ'"),
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон(НСтр("ru = 'correlationId=%1 result=OK orderId=%2'"),
КорреляцияИд, НомерЗаказа));
Проверяйте аутентификацию в первую очередь, до обращения к бизнес-данным.
// Проверка Bearer-токена во входящем запросе
Функция ПроверитьАутентификациюЗапроса(Запрос)
ЗаголовокAuth = Запрос.Заголовки.Получить("Authorization");
Если НЕ ЗначениеЗаполнено(ЗаголовокAuth) Тогда
Возврат Ложь;
КонецЕсли;
Если НЕ СтрНачинаетсяС(ЗаголовокAuth, "Bearer ") Тогда
Возврат Ложь;
КонецЕсли;
ПолученныйТокен = Сред(ЗаголовокAuth, 8); // убираем "Bearer "
УстановитьПривилегированныйРежим(Истина);
ОжидаемыйТокен = ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища("ВходящийAPIТокен", "Токен");
УстановитьПривилегированныйРежим(Ложь);
// Сравнение в постоянное время (защита от timing attack)
// Для простых случаев допустимо прямое сравнение строк
Возврат (ПолученныйТокен = ОжидаемыйТокен);
КонецФункции
// В начале обработчика:
Если НЕ ПроверитьАутентификациюЗапроса(Запрос) Тогда
Возврат ОтветОшибки(401, "UNAUTHORIZED", НСтр("ru = 'Аутентификация не прошла'"));
КонецЕсли;
Изменения контракта без обратной совместимости требуют новой версии.
| Изменение | Совместимость | Действие |
|-----------|--------------|---------|
| Добавить новое поле в ответ | Совместимо | Добавить; документировать |
| Добавить необязательное поле в запрос | Совместимо | Добавить с defaults |
| Удалить или переименовать поле | Несовместимо | Создать /v2/…, поддерживать /v1/… |
| Изменить тип поля | Несовместимо | Создать /v2/…, поддерживать /v1/… |
| Изменить семантику существующего поля | Несовместимо | Создать /v2/… |
Версию указывайте в URL: /api/v1/orders, /api/v2/orders.
| Ошибка | Последствие | Как избежать |
|--------|------------|--------------|
| Секрет в константе конфигурации или реквизите | Утечка при выгрузке конфигурации / базы | Только через ОбщегоНазначения.*ДанныеВБезопасноеХранилище* |
| HTTP-вызов внутри транзакции | Таймаут (30 с) = блокировка всех связанных записей | Выносить HTTP-вызовы за пределы транзакции |
| Нет ключа идемпотентности при повторах | Дублирование данных при сетевой ошибке | Использовать Idempotency-Key или внешний ID с уникальным индексом |
| Возврат стека 1С в теле ошибки | Раскрытие внутреннего устройства системы | Возвращать только стабильный код ошибки и сообщение |
| Ретрай на 4xx-ошибках | Бесполезная нагрузка, возможное повторение конфликта | Ретраить только 5xx и сетевые ошибки |
| Логирование токенов/паролей | Секреты в журнале регистрации | Маскировать перед записью в ЖР |
| Бизнес-логика в обработчике HTTP-сервиса | Невозможность повторного использования и тестирования | Тонкий обработчик + отдельный общий модуль |
| Нет валидации входных данных | Запись некорректных данных в базу | Проверять все обязательные поля до бизнес-операции |
| Триггер | Действие | |---------|----------| | Создаётся HTTP-сервис 1С (входящий) | Применить правила 2, 6, 9, 10 | | Создаётся HTTP-клиент (исходящий REST) | Применить правила 3, 4, 5, 8 | | Настраивается SOAP/Web Service | Применить правило 7, 3 (секреты) | | Проектируется или меняется контракт | Начать с правила 1, применить правило 10 | | Добавляется любая аутентификация | Применить правило 3 + ../security/references/auth.md | | Ревью кода интеграции | Пройти по чеклисту ниже |
ОбщегоНазначения.*ДанныеВБезопасноеХранилище*; нигде не логируются/v2/…)development
1C server maintenance webhooks: container restart and external component cache cleanup
development
Interactive DAP debugging of a single BSL procedure
tools
Rules for using RLM tools for project search and navigation in 1C/BSL
development
Creates web applications and routes on Winow (a web server on OneScript and Autumn). Use when working with a web server on OneScript, routing, or Winow controllers.