REST API в 1С:Предприятие — исчерпывающее руководство разработчика

TL;DR: Исчерпывающий гайд по REST API в 1С — от теории до production deployment. Полный цикл разработки: архитектура (OData vs HTTP-сервисы), проектирование, безопасность (JWT, API keys), обработка ошибок, ретраи, валидация JSON, мониторинг, производительность, версионирование и лучшие практики для PROD. 15000+ слов практических примеров.

Современные интеграции требуют предсказуемых, безопасных и масштабируемых API. Платформа 1С 8.3.20+ предоставляет два подхода: автоматический OData REST слой и полностью кастомные HTTP-сервисы. Это исчерпывающее руководство основано на реальном опыте внедрений в enterprise-среде и покрывает весь жизненный цикл — от архитектурных решений до production deployment. Также см. сравнение методов интеграции 1С и раздел услуг.

Теоретические основы REST в контексте 1С

REST (Representational State Transfer) — архитектурный стиль, основанный на принципах HTTP-протокола. В 1С поддержка REST реализована через два основных механизма: автоматический интерфейс OData и собственные HTTP-сервисы.

Ключевые принципы REST в 1С:

  • Единообразный интерфейс — ресурсы идентифицируются URL-адресами
  • Stateless — сервер не хранит состояние клиента между запросами
  • Кэшируемость — поддержка HTTP-кэширования для оптимизации
  • Клиент-серверная архитектура с четким разделением ответственности
  • Слоистая система — возможность добавления промежуточных слоев (прокси, балансировщики)
  • Единообразие кода по требованию (опционально)

OData vs HTTP-сервисы: когда что использовать

Критерий OData HTTP-сервисы
Скорость разработки ✅ Автоматическая генерация ⚠️ Ручная разработка
Гибкость API ⚠️ Ограниченная ✅ Полный контроль
Производительность ⚠️ Может быть избыточной ✅ Оптимизируется под задачи
Версионирование ❌ Ограниченное ✅ Полная поддержка
Безопасность ⚠️ Базовая ✅ Кастомная логика

Платформа 1С 8.3.20+ автоматически генерирует REST-интерфейс на базе OData 3.0, поддерживая форматы Atom/XML и JSON. Для более гибких решений доступно создание собственных HTTP-сервисов.

💡 Практический совет: Начните с OData для быстрого прототипирования, затем переходите к HTTP-сервисам для production-решений с высокими требованиями к производительности и безопасности.

Объектная модель HTTP в 1С

HTTPСоединение — основа клиентских запросов

Объект HTTPСоединение обеспечивает взаимодействие с внешними REST API. Его ключевые методы соответствуют HTTP-глаголам:

// Создание защищенного соединения с полной настройкой
SSL = Новый ЗащищенноеСоединениеOpenSSL();
Соединение = Новый HTTPСоединение(
    "api.cbr.ru",           // Сервер
    443,                    // Порт (443 для HTTPS)
    ,                       // Пользователь
    ,                       // Пароль  
    ,                       // Прокси
    180,                    // Таймаут (секунды)
    SSL                     // SSL соединение
);

Методы HTTPСоединение:

  • Получить() — выполняет GET-запросы
  • ОтправитьДляОбработки() — выполняет POST-запросы
  • Записать() — выполняет PUT-запросы
  • Удалить() — выполняет DELETE-запросы

HTTPЗапрос и HTTPОтвет — работа с данными

Функция ВыполнитьGETЗапрос(Соединение, АдресРесурса, Заголовки = Неопределено)
    
    Попытка
        Запрос = Новый HTTPЗапрос(АдресРесурса);
        
        // Установка заголовков
        Если Заголовки <> Неопределено Тогда
            Для Каждого Заголовок Из Заголовки Цикл
                Запрос.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);
            КонецЦикла;
        КонецЕсли;
        
        // Установка таймаута для конкретного запроса
        Запрос.УстановитьТаймаут(30); // 30 секунд
        
        Ответ = Соединение.Получить(Запрос);
        
        // Детальная проверка кода ответа
        Если Ответ.КодСостояния >= 200 И Ответ.КодСостояния < 300 Тогда
            Возврат Ответ.ПолучитьТелоКакСтроку("UTF-8");
        ИначеЕсли Ответ.КодСостояния = 404 Тогда
            ВызватьИсключение "Ресурс не найден";
        ИначеЕсли Ответ.КодСостояния = 401 Тогда
            ВызватьИсключение "Требуется аутентификация";
        ИначеЕсли Ответ.КодСостояния = 403 Тогда
            ВызватьИсключение "Доступ запрещен";
        ИначеЕсли Ответ.КодСостояния >= 500 Тогда
            ВызватьИсключение СтрШаблон("Ошибка сервера: %1", Ответ.КодСостояния);
        Иначе
            ВызватьИсключение СтрШаблон("Ошибка HTTP %1: %2", 
                Ответ.КодСостояния, 
                Ответ.СтатусТекст);
        КонецЕсли;
        
    Исключение
        ТекстОшибки = СтрШаблон("Ошибка при выполнении GET-запроса к %1: %2", 
            АдресРесурса, 
            ОписаниеОшибки());
        ЗаписьЖурналаРегистрации("HTTP.Клиент", 
            УровеньЖурналаРегистрации.Ошибка,,,
            ТекстОшибки);
        ВызватьИсключение ТекстОшибки;
    КонецПопытки;
    
КонецФункции

Универсальный HTTP-клиент с расширенными возможностями

Функция ВыполнитьHTTPЗапрос(Знач Настройки) Экспорт
    // Настройки: Сервер, Порт, Метод, Путь, Тело, Заголовки, Таймаут, SSL
    
    Попытка
        // Создание соединения
        Если Настройки.Свойство("SSL") И Настройки.SSL Тогда
            SSL = Новый ЗащищенноеСоединениеOpenSSL();
            Соединение = Новый HTTPСоединение(
                Настройки.Сервер, 
                Настройки.Получить("Порт", 443),
                Настройки.Получить("Пользователь"),
                Настройки.Получить("Пароль"),
                Настройки.Получить("Прокси"),
                Настройки.Получить("Таймаут", 60),
                SSL
            );
        Иначе
            Соединение = Новый HTTPСоединение(
                Настройки.Сервер,
                Настройки.Получить("Порт", 80),
                Настройки.Получить("Пользователь"),
                Настройки.Получить("Пароль"),
                Настройки.Получить("Прокси"),
                Настройки.Получить("Таймаут", 60)
            );
        КонецЕсли;
        
        // Создание запроса
        Запрос = Новый HTTPЗапрос(Настройки.Путь);
        
        // Установка заголовков по умолчанию
        Запрос.Заголовки.Вставить("User-Agent", "1C Enterprise 8.3 REST Client");
        Запрос.Заголовки.Вставить("Accept", "application/json");
        Запрос.Заголовки.Вставить("Accept-Charset", "utf-8");
        
        // Дополнительные заголовки
        Если Настройки.Свойство("Заголовки") Тогда
            Для Каждого Заголовок Из Настройки.Заголовки Цикл
                Запрос.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);
            КонецЦикла;
        КонецЕсли;
        
        // Установка тела для POST/PUT
        Если Настройки.Свойство("Тело") И НЕ ПустаяСтрока(Настройки.Тело) Тогда
            Запрос.УстановитьТелоИзСтроки(Настройки.Тело, "UTF-8");
            Если НЕ Запрос.Заголовки.Свойство("Content-Type") Тогда
                Запрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
            КонецЕсли;
        КонецЕсли;
        
        // Выполнение запроса по методу
        Метод = ВРег(Настройки.Получить("Метод", "GET"));
        
        Если Метод = "GET" Тогда
            Ответ = Соединение.Получить(Запрос);
        ИначеЕсли Метод = "POST" Тогда
            Ответ = Соединение.ОтправитьДляОбработки(Запрос);
        ИначеЕсли Метод = "PUT" Тогда
            Ответ = Соединение.Записать(Запрос);
        ИначеЕсли Метод = "DELETE" Тогда
            Ответ = Соединение.Удалить(Запрос);
        Иначе
            ВызватьИсключение СтрШаблон("Неподдерживаемый HTTP-метод: %1", Метод);
        КонецЕсли;
        
        // Формирование результата
        Результат = Новый Структура;
        Результат.Вставить("КодСостояния", Ответ.КодСостояния);
        Результат.Вставить("СтатусТекст", Ответ.СтатусТекст);
        Результат.Вставить("Заголовки", Ответ.Заголовки);
        Результат.Вставить("Тело", Ответ.ПолучитьТелоКакСтроку("UTF-8"));
        
        // Попытка десериализации JSON
        Если СтрНачинаетсяС(Ответ.Заголовки.Получить("Content-Type"), "application/json") Тогда
            Попытка
                Результат.Вставить("JSON", ДесериализоватьИзJSON(Результат.Тело));
            Исключение
                // JSON невалидный, оставляем как текст
            КонецПопытки;
        КонецЕсли;
        
        Возврат Результат;
        
    Исключение
        ВызватьИсключение СтрШаблон("Ошибка HTTP-запроса %1 %2: %3", 
            Настройки.Получить("Метод", "GET"),
            Настройки.Сервер + Настройки.Путь,
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Практический пример: интеграция с ЦБ РФ

Создадим универсальное решение для получения курсов валют от Центрального банка России с полной обработкой ошибок и кэшированием:

Функция ПолучитьКурсыВалютЦБРФ(НаДату = Неопределено) Экспорт
    
    // Используем текущую дату, если не указана другая
    Если НаДату = Неопределено Тогда
        НаДату = ТекущаяДата();
    КонецЕсли;
    
    СтрокаДаты = Формат(НаДату, "ДФ=dd/MM/yyyy");
    АдресЗапроса = СтрШаблон("/scripts/XML_daily.asp?date_req=%1", СтрокаДаты);
    
    Попытка
        // Создание соединения
        SSL = Новый ЗащищенноеСоединениеOpenSSL();
        Соединение = Новый HTTPСоединение("www.cbr.ru", 443,,, , 30, SSL);
        
        // Выполнение запроса
        Запрос = Новый HTTPЗапрос(АдресЗапроса);
        Запрос.Заголовки.Вставить("User-Agent", "1C Enterprise 8.3");
        Запрос.Заголовки.Вставить("Accept", "text/xml, application/xml");
        
        Ответ = Соединение.Получить(Запрос);
        
        Если Ответ.КодСостояния <> 200 Тогда
            ВызватьИсключение СтрШаблон("ЦБ РФ вернул код %1: %2", 
                Ответ.КодСостояния, Ответ.СтатусТекст);
        КонецЕсли;
        
        // Парсинг XML ответа
        ТекстXML = Ответ.ПолучитьТелоКакСтроку("Windows-1251");
        ЧтениеXML = Новый ЧтениеXML;
        ЧтениеXML.УстановитьСтроку(ТекстXML);
        
        ДокументXML = ПостроительDOM.ПрочитатьДокумент(ЧтениеXML);
        
        // Проверяем корневой элемент
        КорневойЭлемент = ДокументXML.ДокументЭлемент;
        Если КорневойЭлемент.ИмяУзла <> "ValCurs" Тогда
            ВызватьИсключение "Некорректный формат ответа от ЦБ РФ";
        КонецЕсли;
        
        Курсы = Новый Массив;
        УзлыВалют = ДокументXML.НайтиУзлыПоИмени("Valute");
        
        Для Каждого УзелВалюты Из УзлыВалют Цикл
            КурсВалюты = Новый Структура;
            КурсВалюты.Вставить("Код", УзелВалюты.АтрибутыЭлемента.Получить("ID"));
            КурсВалюты.Вставить("КодЦифра", УзелВалюты.НайтиДочернийЭлемент("NumCode").ТекстовоеСодержимое);
            КурсВалюты.Вставить("КодСимвол", УзелВалюты.НайтиДочернийЭлемент("CharCode").ТекстовоеСодержимое);
            КурсВалюты.Вставить("Наименование", УзелВалюты.НайтиДочернийЭлемент("Name").ТекстовоеСодержимое);
            
            НоминалТекст = УзелВалюты.НайтиДочернийЭлемент("Nominal").ТекстовоеСодержимое;
            КурсТекст = УзелВалюты.НайтиДочернийЭлемент("Value").ТекстовоеСодержимое;
            
            // Обработка числовых значений с учетом локали
            КурсВалюты.Вставить("Номинал", Число(НоминалТекст));
            КурсВалюты.Вставить("Курс", Число(СтрЗаменить(КурсТекст, ",", ".")));
            КурсВалюты.Вставить("КурсЗаЕдиницу", КурсВалюты.Курс / КурсВалюты.Номинал);
            КурсВалюты.Вставить("ДатаКурса", НаДату);
            
            Курсы.Добавить(КурсВалюты);
        КонецЦикла;
        
        // Логирование успешного получения
        ЗаписьЖурналаРегистрации("ИнтеграцияЦБРФ", 
            УровеньЖурналаРегистрации.Информация,,,
            СтрШаблон("Получено курсов валют: %1 на дату %2", 
                Курсы.Количество(), Формат(НаДату, "ДЛФ=D")));
        
        Возврат Курсы;
        
    Исключение
        ТекстОшибки = СтрШаблон("Не удалось получить курсы валют на %1: %2", 
            Формат(НаДату, "ДЛФ=D"), 
            ОписаниеОшибки());
        ЗаписьЖурналаРегистрации("ИнтеграцияЦБРФ", 
            УровеньЖурналаРегистрации.Ошибка,,,
            ТекстОшибки);
        ВызватьИсключение ТекстОшибки;
    КонецПопытки;
    
КонецФункции

Кэширование курсов валют

Для оптимизации добавим кэширование с автоматической инвалидацией:

Функция ПолучитьКурсыВалютСКэшем(НаДату = Неопределено) Экспорт
    
    Если НаДату = Неопределено Тогда
        НаДату = ТекущаяДата();
    КонецЕсли;
    
    // Проверяем кэш
    КлючКэша = СтрШаблон("КурсыЦБРФ_%1", Формат(НаДату, "ДФ=yyyyMMdd"));
    
    Если ХранилищеОбщихНастроек.Загрузить("КэшКурсовВалют", КлючКэша) <> Неопределено Тогда
        КэшированныеДанные = ХранилищеОбщихНастроек.Загрузить("КэшКурсовВалют", КлючКэша);
        
        // Проверяем актуальность кэша (не старше 1 часа для текущей даты)
        Если НаДату = ТекущаяДата() Тогда
            ВремяЖизниКэша = 3600; // 1 час в секундах
        Иначе
            ВремяЖизниКэша = 86400; // 24 часа для исторических данных
        КонецЕсли;
        
        Если ТекущаяУниверсальнаяДата() - КэшированныеДанные.ВремяСоздания < ВремяЖизниКэша Тогда
            ЗаписьЖурналаРегистрации("КэшКурсовВалют", 
                УровеньЖурналаРегистрации.Информация,,,
                СтрШаблон("Использован кэш для даты %1", Формат(НаДату, "ДЛФ=D")));
            Возврат КэшированныеДанные.Курсы;
        КонецЕсли;
    КонецЕсли;
    
    // Получаем свежие данные
    Курсы = ПолучитьКурсыВалютЦБРФ(НаДату);
    
    // Сохраняем в кэш
    ДанныеДляКэша = Новый Структура;
    ДанныеДляКэша.Вставить("Курсы", Курсы);
    ДанныеДляКэша.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
    
    ХранилищеОбщихНастроек.Сохранить("КэшКурсовВалют", КлючКэша, ДанныеДляКэша);
    
    Возврат Курсы;
    
КонецФункции

Фильтрация и поиск валют

// Получение курса конкретной валюты
Функция ПолучитьКурсВалюты(КодВалюты, НаДату = Неопределено) Экспорт
    
    Курсы = ПолучитьКурсыВалютСКэшем(НаДату);
    
    Для Каждого КурсВалюты Из Курсы Цикл
        Если ВРег(КурсВалюты.КодСимвол) = ВРег(КодВалюты) 
            ИЛИ КурсВалюты.КодЦифра = КодВалюты Тогда
            Возврат КурсВалюты;
        КонецЕсли;
    КонецЦикла;
    
    ВызватьИсключение СтрШаблон("Валюта с кодом %1 не найдена", КодВалюты);
    
КонецФункции

// Конвертация валют
Функция КонвертироватьВалюту(Сумма, ВалютаИз, ВалютаВ, НаДату = Неопределено) Экспорт
    
    Если ВРег(ВалютаИз) = ВРег(ВалютаВ) Тогда
        Возврат Сумма; // Одинаковые валюты
    КонецЕсли;
    
    КурсИз = 1; // RUB по умолчанию
    КурсВ = 1;
    
    Если ВРег(ВалютаИз) <> "RUB" Тогда
        КурсВалютыИз = ПолучитьКурсВалюты(ВалютаИз, НаДату);
        КурсИз = КурсВалютыИз.КурсЗаЕдиницу;
    КонецЕсли;
    
    Если ВРег(ВалютаВ) <> "RUB" Тогда
        КурсВалютыВ = ПолучитьКурсВалюты(ВалютаВ, НаДату);
        КурсВ = КурсВалютыВ.КурсЗаЕдиницу;
    КонецЕсли;
    
    // Сначала конвертируем в рубли, потом в целевую валюту
    СуммаВРублях = Сумма * КурсИз;
    РезультатКонвертации = СуммаВРублях / КурсВ;
    
    Возврат Окр(РезультатКонвертации, 2);
    
КонецФункции

Создание собственных HTTP-сервисов

Структура HTTP-сервиса

HTTP-сервис в 1С состоит из объекта конфигурации и модуля с обработчиками запросов. URL формируется по шаблону:

http://host/base/hs/корневойURL/относительныйURL

Создание ресурса "Клиенты" - полная реализация

Модуль HTTP-сервиса "КлиентыAPI"

// Получение списка клиентов с продвинутой фильтрацией
Функция КлиентыGET(Запрос)
    
    Попытка
        // Логирование входящего запроса
        ЛогироватьВходящийЗапрос(Запрос);
        
        СтруктураОтвета = Новый Структура;
        
        // Получение параметров пагинации
        Лимит = Мин(Число(Запрос.ПараметрыЗапроса.Получить("limit")), 100); // Максимум 100
        Если Лимит = 0 Тогда Лимит = 20; КонецЕсли; // По умолчанию 20
        
        Смещение = Число(Запрос.ПараметрыЗапроса.Получить("offset"));
        
        // Параметры фильтрации
        Поиск = Запрос.ПараметрыЗапроса.Получить("search");
        ТипКлиента = Запрос.ПараметрыЗапроса.Получить("type"); // individual/legal
        Город = Запрос.ПараметрыЗапроса.Получить("city");
        Активные = Запрос.ПараметрыЗапроса.Получить("active"); // true/false
        
        // Параметры сортировки
        СортировкаПоле = Запрос.ПараметрыЗапроса.Получить("sort");
        СортировкаНаправление = ВРег(Запрос.ПараметрыЗапроса.Получить("order")) = "DESC";
        
        // Формирование запроса к базе с динамическими условиями
        ТекстЗапроса = 
        "ВЫБРАТЬ
        |    Клиенты.Ссылка КАК ID,
        |    Клиенты.Наименование,
        |    Клиенты.ИНН,
        |    Клиенты.КПП,
        |    Клиенты.Телефон,
        |    Клиенты.АдресЭлектроннойПочты КАК Email,
        |    Клиенты.ЮрФизЛицо,
        |    Клиенты.Город,
        |    Клиенты.ДатаРегистрации,
        |    НЕ Клиенты.ПометкаУдаления КАК Активен
        |ИЗ
        |    Справочник.Контрагенты КАК Клиенты
        |ГДЕ
        |    НЕ Клиенты.ПометкаУдаления";
        
        // Динамическое добавление условий
        Запрос = Новый Запрос;
        
        Если НЕ ПустаяСтрока(Поиск) Тогда
            ТекстЗапроса = ТекстЗапроса + "
            |    И (Клиенты.Наименование ПОДОБНО &Поиск
            |         ИЛИ Клиенты.ИНН ПОДОБНО &Поиск
            |         ИЛИ Клиенты.Телефон ПОДОБНО &Поиск)";
            Запрос.УстановитьПараметр("Поиск", "%" + Поиск + "%");
        КонецЕсли;
        
        Если НЕ ПустаяСтрока(ТипКлиента) Тогда
            Если ВРег(ТипКлиента) = "INDIVIDUAL" Тогда
                ТекстЗапроса = ТекстЗапроса + "
                |    И Клиенты.ЮрФизЛицо = ЗНАЧЕНИЕ(Перечисление.ЮрФизЛицо.ФизЛицо)";
            ИначеЕсли ВРег(ТипКлиента) = "LEGAL" Тогда
                ТекстЗапроса = ТекстЗапроса + "
                |    И Клиенты.ЮрФизЛицо = ЗНАЧЕНИЕ(Перечисление.ЮрФизЛицо.ЮрЛицо)";
            КонецЕсли;
        КонецЕсли;
        
        Если НЕ ПустаяСтрока(Город) Тогда
            ТекстЗапроса = ТекстЗапроса + "
            |    И Клиенты.Город ПОДОБНО &Город";
            Запрос.УстановитьПараметр("Город", "%" + Город + "%");
        КонецЕсли;
        
        // Сортировка
        ПолеСортировки = "Клиенты.Наименование"; // По умолчанию
        Если НЕ ПустаяСтрока(СортировкаПоле) Тогда
            Если ВРег(СортировкаПоле) = "NAME" Тогда
                ПолеСортировки = "Клиенты.Наименование";
            ИначеЕсли ВРег(СортировкаПоле) = "DATE" Тогда
                ПолеСортировки = "Клиенты.ДатаРегистрации";
            ИначеЕсли ВРег(СортировкаПоле) = "CITY" Тогда
                ПолеСортировки = "Клиенты.Город";
            КонецЕсли;
        КонецЕсли;
        
        НаправлениеСортировки = ?(СортировкаНаправление, "УБЫВ", "ВОЗР");
        ТекстЗапроса = ТекстЗапроса + "
        |УПОРЯДОЧИТЬ ПО
        |    " + ПолеСортировки + " " + НаправлениеСортировки;
        
        Запрос.Текст = ТекстЗапроса;
        
        РезультатЗапроса = Запрос.Выполнить();
        ОбщееКоличество = РезультатЗапроса.Количество();
        
        Выборка = РезультатЗапроса.Выбрать();
        
        // Пропускаем записи до смещения
        Для Счетчик = 1 По Смещение Цикл
            Если НЕ Выборка.Следующий() Тогда
                Прервать;
            КонецЕсли;
        КонецЦикла;
        
        МассивКлиентов = Новый Массив;
        СчетчикВыбранных = 0;
        
        Пока Выборка.Следующий() И СчетчикВыбранных < Лимит Цикл
            Клиент = Новый Структура;
            Клиент.Вставить("id", XMLСтрока(Выборка.ID));
            Клиент.Вставить("name", Выборка.Наименование);
            Клиент.Вставить("inn", Выборка.ИНН);
            Клиент.Вставить("kpp", Выборка.КПП);
            Клиент.Вставить("phone", Выборка.Телефон);
            Клиент.Вставить("email", Выборка.Email);
            Клиент.Вставить("type", ?(Выборка.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ФизЛицо, "individual", "legal"));
            Клиент.Вставить("city", Выборка.Город);
            Клиент.Вставить("registrationDate", XMLСтрока(Выборка.ДатаРегистрации));
            Клиент.Вставить("active", Выборка.Активен);
            
            МассивКлиентов.Добавить(Клиент);
            СчетчикВыбранных = СчетчикВыбранных + 1;
        КонецЦикла;
        
        // Метаданные пагинации
        СтруктураОтвета.Вставить("data", МассивКлиентов);
        СтруктураОтвета.Вставить("pagination", Новый Структура);
        СтруктураОтвета.pagination.Вставить("total", ОбщееКоличество);
        СтруктураОтвета.pagination.Вставить("limit", Лимит);
        СтруктураОтвета.pagination.Вставить("offset", Смещение);
        СтруктураОтвета.pagination.Вставить("hasNext", (Смещение + Лимит) < ОбщееКоличество);
        СтруктураОтвета.pagination.Вставить("hasPrev", Смещение > 0);
        
        // Сериализация в JSON
        СтрокаJSON = СериализоватьВJSON(СтруктураОтвета);
        
        Ответ = Новый HTTPСервисОтвет(200);
        Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
        Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
        Ответ.Заголовки.Вставить("Cache-Control", "public, max-age=300"); // 5 минут кэша
        Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8", 
            ИспользованиеByteOrderMark.НеИспользовать);
            
        // Логирование успешного выполнения
        ОтправитьМетрикиAPI("api_clients_get_success", 1, 
            Новый Структура("method", "GET"));
            
        Возврат Ответ;
        
    Исключение
        Возврат СформироватьОтветОбОшибке(500, "Внутренняя ошибка сервера", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Создание нового клиента с валидацией

Функция КлиентыPOST(Запрос)
    
    Попытка
        НачатьТранзакцию();
        
        // Парсинг JSON из тела запроса
        ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
        
        Если ПустаяСтрока(ТелоЗапроса) Тогда
            ОтменитьТранзакцию();
            Возврат СформироватьОтветОбОшибке(400, "Bad Request", 
                "Тело запроса не может быть пустым");
        КонецЕсли;
        
        ДанныеКлиента = ДесериализоватьИзJSON(ТелоЗапроса);
        
        // Схема валидации
        СхемаВалидации = Новый Структура;
        СхемаВалидации.Вставить("ОбязательныеПоля", Новый Массив);
        СхемаВалидации.ОбязательныеПоля.Добавить("name");
        СхемаВалидации.ОбязательныеПоля.Добавить("type");
        
        СхемаВалидации.Вставить("ТипыПолей", Новый Структура);
        СхемаВалидации.ТипыПолей.Вставить("name", "Строка");
        СхемаВалидации.ТипыПолей.Вставить("type", "Строка");
        СхемаВалидации.ТипыПолей.Вставить("inn", "Строка");
        СхемаВалидации.ТипыПолей.Вставить("email", "Строка");
        
        // Валидация
        РезультатВалидации = ВалидироватьJSONПоСхеме(ТелоЗапроса, СхемаВалидации);
        Если НЕ РезультатВалидации.Валиден Тогда
            ОтменитьТранзакцию();
            Возврат СформироватьОтветОбОшибке(400, "Ошибка валидации", 
                СтрСоединить(РезультатВалидации.Ошибки, "; "));
        КонецЕсли;
        
        // Дополнительная бизнес-валидация
        Если ДанныеКлиента.Свойство("type") Тогда
            Если НЕ (ВРег(ДанныеКлиента.type) = "INDIVIDUAL" ИЛИ ВРег(ДанныеКлиента.type) = "LEGAL") Тогда
                ОтменитьТранзакцию();
                Возврат СформироватьОтветОбОшибке(400, "Bad Request", 
                    "Поле 'type' должно быть 'individual' или 'legal'");
            КонецЕсли;
        КонецЕсли;
        
        // Проверка уникальности ИНН
        Если ДанныеКлиента.Свойство("inn") И НЕ ПустаяСтрока(ДанныеКлиента.inn) Тогда
            Запрос = Новый Запрос;
            Запрос.Текст = 
            "ВЫБРАТЬ ПЕРВЫЕ 1
            |    Контрагенты.Ссылка
            |ИЗ
            |    Справочник.Контрагенты КАК Контрагенты
            |ГДЕ
            |    Контрагенты.ИНН = &ИНН
            |    И НЕ Контрагенты.ПометкаУдаления";
            Запрос.УстановитьПараметр("ИНН", ДанныеКлиента.inn);
            
            Если НЕ Запрос.Выполнить().Пустой() Тогда
                ОтменитьТранзакцию();
                Возврат СформироватьОтветОбОшибке(409, "Conflict", 
                    "Клиент с таким ИНН уже существует");
            КонецЕсли;
        КонецЕсли;
        
        // Создание нового клиента
        НовыйКлиент = Справочники.Контрагенты.СоздатьЭлемент();
        НовыйКлиент.Наименование = ДанныеКлиента.name;
        
        Если ВРег(ДанныеКлиента.type) = "INDIVIDUAL" Тогда
            НовыйКлиент.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ФизЛицо;
        Иначе
            НовыйКлиент.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ЮрЛицо;
        КонецЕсли;
        
        // Заполнение дополнительных полей
        Если ДанныеКлиента.Свойство("inn") Тогда
            НовыйКлиент.ИНН = ДанныеКлиента.inn;
        КонецЕсли;
        
        Если ДанныеКлиента.Свойство("kpp") Тогда
            НовыйКлиент.КПП = ДанныеКлиента.kpp;
        КонецЕсли;
        
        Если ДанныеКлиента.Свойство("phone") Тогда
            НовыйКлиент.Телефон = ДанныеКлиента.phone;
        КонецЕсли;
        
        Если ДанныеКлиента.Свойство("email") Тогда
            НовыйКлиент.АдресЭлектроннойПочты = ДанныеКлиента.email;
        КонецЕсли;
        
        Если ДанныеКлиента.Свойство("city") Тогда
            НовыйКлиент.Город = ДанныеКлиента.city;
        КонецЕсли;
        
        НовыйКлиент.ДатаРегистрации = ТекущаяДата();
        
        // Проверка заполнения и запись
        НовыйКлиент.ПроверитьЗаполнение();
        НовыйКлиент.Записать();
        
        ЗафиксироватьТранзакцию();
        
        // Формирование ответа с данными созданного клиента
        СозданныйКлиент = Новый Структура;
        СозданныйКлиент.Вставить("id", XMLСтрока(НовыйКлиент.Ссылка));
        СозданныйКлиент.Вставить("name", НовыйКлиент.Наименование);
        СозданныйКлиент.Вставить("type", ?(НовыйКлиент.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ФизЛицо, "individual", "legal"));
        СозданныйКлиент.Вставить("inn", НовыйКлиент.ИНН);
        СозданныйКлиент.Вставить("kpp", НовыйКлиент.КПП);
        СозданныйКлиент.Вставить("phone", НовыйКлиент.Телефон);
        СозданныйКлиент.Вставить("email", НовыйКлиент.АдресЭлектроннойПочты);
        СозданныйКлиент.Вставить("city", НовыйКлиент.Город);
        СозданныйКлиент.Вставить("registrationDate", XMLСтрока(НовыйКлиент.ДатаРегистрации));
        СозданныйКлиент.Вставить("active", Истина);
        
        СтрокаJSON = СериализоватьВJSON(СозданныйКлиент);
        
        Ответ = Новый HTTPСервисОтвет(201); // Created
        Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
        Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
        Ответ.Заголовки.Вставить("Location", СтрШаблон("/hs/clients/%1", XMLСтрока(НовыйКлиент.Ссылка)));
        Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8", 
            ИспользованиеByteOrderMark.НеИспользовать);
            
        // Метрики
        ОтправитьМетрикиAPI("api_clients_post_success", 1);
            
        Возврат Ответ;
        
    Исключение
        ОтменитьТранзакцию();
        Возврат СформироватьОтветОбОшибке(500, "Ошибка при создании клиента", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Обновление клиента (PUT)

Функция КлиентыPUT(Запрос)
    
    Попытка
        НачатьТранзакцию();
        
        // Получение ID клиента из URL
        ИДКлиента = Запрос.ПараметрыURL.Получить("id");
        Если ПустаяСтрока(ИДКлиента) Тогда
            ОтменитьТранзакцию();
            Возврат СформироватьОтветОбОшибке(400, "Bad Request", 
                "Не указан ID клиента");
        КонецЕсли;
        
        // Поиск клиента
        СсылкаКлиента = XMLЗначение(Тип("СправочникСсылка.Контрагенты"), ИДКлиента);
        Если НЕ ЗначениеЗаполнено(СсылкаКлиента) ИЛИ СсылкаКлиента.ПометкаУдаления Тогда
            ОтменитьТранзакцию();
            Возврат СформироватьОтветОбОшибке(404, "Not Found", 
                "Клиент не найден");
        КонецЕсли;
        
        // Парсинг данных
        ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
        ДанныеКлиента = ДесериализоватьИзJSON(ТелоЗапроса);
        
        // Открытие объекта для изменения
        ОбъектКлиента = СсылкаКлиента.ПолучитьОбъект();
        
        // Обновление полей
        Если ДанныеКлиента.Свойство("name") Тогда
            ОбъектКлиента.Наименование = ДанныеКлиента.name;
        КонецЕсли;
        
        Если ДанныеКлиента.Свойство("inn") Тогда
            ОбъектКлиента.ИНН = ДанныеКлиента.inn;
        КонецЕсли;
        
        Если ДанныеКлиента.Свойство("phone") Тогда
            ОбъектКлиента.Телефон = ДанныеКлиента.phone;
        КонецЕсли;
        
        Если ДанныеКлиента.Свойство("email") Тогда
            ОбъектКлиента.АдресЭлектроннойПочты = ДанныеКлиента.email;
        КонецЕсли;
        
        ОбъектКлиента.Записать();
        ЗафиксироватьТранзакцию();
        
        // Возврат обновленных данных
        ОбновленныйКлиент = ПолучитьДанныеКлиента(СсылкаКлиента);
        СтрокаJSON = СериализоватьВJSON(ОбновленныйКлиент);
        
        Ответ = Новый HTTPСервисОтвет(200);
        Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
        Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8", 
            ИспользованиеByteOrderMark.НеИспользовать);
            
        Возврат Ответ;
        
    Исключение
        ОтменитьТранзакцию();
        Возврат СформироватьОтветОбОшибке(500, "Ошибка при обновлении клиента", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Работа с JSON и сериализацией

Универсальный класс для работы с JSON

// Общий модуль УниверсальныйJSON

Функция СериализоватьВJSON(Данные) Экспорт
    
    Попытка
        ЗаписьJSON = Новый ЗаписьJSON;
        ЗаписьJSON.УстановитьСтроку();
        
        НастройкиСериализации = Новый НастройкиСериализацииJSON;
        НастройкиСериализации.ФорматДаты = ФорматДатыJSON.ISO;
        НастройкиСериализации.ВариантЗаписиДаты = ВариантЗаписиДатыJSON.Универсальная;
        НастройкиСериализации.ПереносСтрок = ПереносСтрокJSON.Нет;
        НастройкиСериализации.ПробелыEcmascript = Ложь;
        НастройкиСериализации.ЭкранированиеСлешей = ЭкранированиеСлешейJSON.НеЭкранировать;
        
        ЗаписатьJSON(ЗаписьJSON, Данные, НастройкиСериализации);
        
        Результат = ЗаписьJSON.Закрыть();
        Возврат Результат;
        
    Исключение
        ВызватьИсключение СтрШаблон("Ошибка сериализации в JSON: %1", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Функция ДесериализоватьИзJSON(СтрокаJSON) Экспорт
    
    Попытка
        ЧтениеJSON = Новый ЧтениеJSON;
        ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
        
        НастройкиЧтения = Новый НастройкиЧтенияJSON;
        НастройкиЧтения.ЧтениеДатыИзСтроки = ЧтениеДатыИзСтроки.ПоДанным;
        НастройкиЧтения.ИменаСвойствВВерхнемРегистре = Ложь;
        
        Результат = ПрочитатьJSON(ЧтениеJSON, НастройкиЧтения);
        
        ЧтениеJSON.Закрыть();
        Возврат Результат;
        
    Исключение
        ВызватьИсключение СтрШаблон("Ошибка десериализации JSON: %1", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

// Сериализация объекта 1С в JSON с использованием XDTO
Функция СериализоватьОбъект1СВJSON(Объект) Экспорт
    
    Попытка
        СериализаторXDTO = Новый СериализаторXDTO(ФабрикаXDTO);
        
        НастройкиСериализации = Новый НастройкиСериализацииJSON;
        НастройкиСериализации.ФорматДаты = ФорматДатыJSON.ISO;
        НастройкиСериализации.ВариантЗаписиДаты = ВариантЗаписиДатыJSON.Универсальная;
        
        СериализованныйОбъект = СериализаторXDTO.ЗаписатьJSON(Объект, 
            НастройкиСериализации);
            
        Возврат СериализованныйОбъект;
        
    Исключение
        ВызватьИсключение СтрШаблон("Ошибка сериализации объекта 1С: %1", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

// Десериализация объекта 1С из JSON
Функция ДесериализоватьОбъект1СИзJSON(СтрокаJSON, ТипОбъекта) Экспорт
    
    Попытка
        СериализаторXDTO = Новый СериализаторXDTO(ФабрикаXDTO);
        
        НастройкиЧтения = Новый НастройкиЧтенияJSON;
        НастройкиЧтения.ЧтениеДатыИзСтроки = ЧтениеДатыИзСтроки.ПоДанным;
        
        ВосстановленныйОбъект = СериализаторXDTO.ПрочитатьJSON(
            СтрокаJSON, ТипОбъекта, НастройкиЧтения);
            
        Возврат ВосстановленныйОбъект;
        
    Исключение
        ВызватьИсключение СтрШаблон("Ошибка десериализации объекта 1С: %1", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Продвинутая валидация JSON

Функция ВалидироватьJSONПоСхеме(СтрокаJSON, СхемаВалидации) Экспорт
    
    СтруктураРезультата = Новый Структура;
    СтруктураРезультата.Вставить("Валиден", Ложь);
    СтруктураРезультата.Вставить("Ошибки", Новый Массив);
    СтруктураРезультата.Вставить("Предупреждения", Новый Массив);
    
    Попытка
        ДанныеJSON = ДесериализоватьИзJSON(СтрокаJSON);
        
        // Проверка обязательных полей
        Если СхемаВалидации.Свойство("ОбязательныеПоля") Тогда
            Для Каждого ОбязательноеПоле Из СхемаВалидации.ОбязательныеПоля Цикл
                Если НЕ ДанныеJSON.Свойство(ОбязательноеПоле) Тогда
                    СтруктураРезультата.Ошибки.Добавить(
                        СтрШаблон("Отсутствует обязательное поле: %1", 
                            ОбязательноеПоле));
                ИначеЕсли ТипЗнч(ДанныеJSON[ОбязательноеПоле]) = Тип("Строка") 
                    И ПустаяСтрока(ДанныеJSON[ОбязательноеПоле]) Тогда
                    СтруктураРезультата.Ошибки.Добавить(
                        СтрШаблон("Обязательное поле '%1' не может быть пустым", 
                            ОбязательноеПоле));
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        // Проверка типов данных
        Если СхемаВалидации.Свойство("ТипыПолей") Тогда
            Для Каждого ОписаниеТипа Из СхемаВалидации.ТипыПолей Цикл
                ИмяПоля = ОписаниеТипа.Ключ;
                ОжидаемыйТип = ОписаниеТипа.Значение;
                
                Если ДанныеJSON.Свойство(ИмяПоля) Тогда
                    ФактическийТип = ТипЗнч(ДанныеJSON[ИмяПоля]);
                    
                    Если ОжидаемыйТип = "Строка" И ФактическийТип <> Тип("Строка") Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' должно быть строкой", ИмяПоля));
                    ИначеЕсли ОжидаемыйТип = "Число" И ФактическийТип <> Тип("Число") Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' должно быть числом", ИмяПоля));
                    ИначеЕсли ОжидаемыйТип = "Булево" И ФактическийТип <> Тип("Булево") Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' должно быть булевым значением", ИмяПоля));
                    ИначеЕсли ОжидаемыйТип = "Дата" И ФактическийТип <> Тип("Дата") Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' должно быть датой", ИмяПоля));
                    КонецЕсли;
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        // Проверка ограничений длины строк
        Если СхемаВалидации.Свойство("МаксимальныеДлины") Тогда
            Для Каждого ОграничениеДлины Из СхемаВалидации.МаксимальныеДлины Цикл
                ИмяПоля = ОграничениеДлины.Ключ;
                МаксДлина = ОграничениеДлины.Значение;
                
                Если ДанныеJSON.Свойство(ИмяПоля) И ТипЗнч(ДанныеJSON[ИмяПоля]) = Тип("Строка") Тогда
                    Если СтрДлина(ДанныеJSON[ИмяПоля]) > МаксДлина Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' превышает максимальную длину %2 символов", 
                                ИмяПоля, МаксДлина));
                    КонецЕсли;
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        // Проверка регулярных выражений
        Если СхемаВалидации.Свойство("Шаблоны") Тогда
            Для Каждого ШаблонПроверки Из СхемаВалидации.Шаблоны Цикл
                ИмяПоля = ШаблонПроверки.Ключ;
                Шаблон = ШаблонПроверки.Значение;
                
                Если ДанныеJSON.Свойство(ИмяПоля) И ТипЗнч(ДанныеJSON[ИмяПоля]) = Тип("Строка") Тогда
                    Если НЕ СтрСоответствуетШаблону(ДанныеJSON[ИмяПоля], Шаблон) Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' не соответствует требуемому формату", ИмяПоля));
                    КонецЕсли;
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        // Проверка допустимых значений (enum)
        Если СхемаВалидации.Свойство("ДопустимыеЗначения") Тогда
            Для Каждого ОграничениеЗначений Из СхемаВалидации.ДопустимыеЗначения Цикл
                ИмяПоля = ОграничениеЗначений.Ключ;
                ДопустимыеЗначения = ОграничениеЗначений.Значение;
                
                Если ДанныеJSON.Свойство(ИмяПоля) Тогда
                    Если ДопустимыеЗначения.Найти(ДанныеJSON[ИмяПоля]) = Неопределено Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' содержит недопустимое значение. Допустимы: %2", 
                                ИмяПоля, СтрСоединить(ДопустимыеЗначения, ", ")));
                    КонецЕсли;
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        // Проверка числовых диапазонов
        Если СхемаВалидации.Свойство("ЧисловыеДиапазоны") Тогда
            Для Каждого Диапазон Из СхемаВалидации.ЧисловыеДиапазоны Цикл
                ИмяПоля = Диапазон.Ключ;
                ПараметрыДиапазона = Диапазон.Значение;
                
                Если ДанныеJSON.Свойство(ИмяПоля) И ТипЗнч(ДанныеJSON[ИмяПоля]) = Тип("Число") Тогда
                    Значение = ДанныеJSON[ИмяПоля];
                    
                    Если ПараметрыДиапазона.Свойство("Минимум") И Значение < ПараметрыДиапазона.Минимум Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' не может быть меньше %2", ИмяПоля, ПараметрыДиапазона.Минимум));
                    КонецЕсли;
                    
                    Если ПараметрыДиапазона.Свойство("Максимум") И Значение > ПараметрыДиапазона.Максимум Тогда
                        СтруктураРезультата.Ошибки.Добавить(
                            СтрШаблон("Поле '%1' не может быть больше %2", ИмяПоля, ПараметрыДиапазона.Максимум));
                    КонецЕсли;
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        СтруктураРезультата.Валиден = (СтруктураРезультата.Ошибки.Количество() = 0);
        
    Исключение
        СтруктураРезультата.Ошибки.Добавить(
            СтрШаблон("Ошибка валидации JSON: %1", ОписаниеОшибки()));
    КонецПопытки;
    
    Возврат СтруктураРезультата;
    
КонецФункции

Работа с вложенными объектами и массивами

// Функция для работы с комплексными JSON-структурами
Функция ОбработатьВложенныйJSON(ДанныеJSON, СхемаОбработки) Экспорт
    
    Результат = Новый Структура;
    
    Для Каждого ПравилоОбработки Из СхемаОбработки Цикл
        ИмяПоля = ПравилоОбработки.Ключ;
        ПараметрыОбработки = ПравилоОбработки.Значение;
        
        Если ДанныеJSON.Свойство(ИмяПоля) Тогда
            Значение = ДанныеJSON[ИмяПоля];
            
            Если ПараметрыОбработки.ТипОбработки = "Массив" Тогда
                // Обработка массива объектов
                МассивРезультатов = Новый Массив;
                
                Для Каждого ЭлементМассива Из Значение Цикл
                    ОбработанныйЭлемент = ОбработатьВложенныйJSON(
                        ЭлементМассива, ПараметрыОбработки.СхемаЭлемента);
                    МассивРезультатов.Добавить(ОбработанныйЭлемент);
                КонецЦикла;
                
                Результат.Вставить(ИмяПоля, МассивРезультатов);
                
            ИначеЕсли ПараметрыОбработки.ТипОбработки = "Объект" Тогда
                // Рекурсивная обработка вложенного объекта
                ВложенныйРезультат = ОбработатьВложенныйJSON(
                    Значение, ПараметрыОбработки.СхемаОбъекта);
                Результат.Вставить(ИмяПоля, ВложенныйРезультат);
                
            ИначеЕсли ПараметрыОбработки.ТипОбработки = "Дата" Тогда
                // Преобразование строки в дату
                Попытка
                    ДатаЗначение = XMLЗначение(Тип("Дата"), Значение);
                    Результат.Вставить(ИмяПоля, ДатаЗначение);
                Исключение
                    Результат.Вставить(ИмяПоля, Неопределено);
                КонецПопытки;
                
            ИначеЕсли ПараметрыОбработки.ТипОбработки = "СсылкаНаОбъект" Тогда
                // Преобразование UUID в ссылку на объект 1С
                Попытка
                    ТипСсылки = ПараметрыОбработки.ТипСсылки;
                    СсылкаОбъекта = XMLЗначение(ТипСсылки, Значение);
                    Результат.Вставить(ИмяПоля, СсылкаОбъекта);
                Исключение
                    Результат.Вставить(ИмяПоля, Неопределено);
                КонецПопытки;
                
            Иначе
                // Простое копирование значения
                Результат.Вставить(ИмяПоля, Значение);
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;
    
    Возврат Результат;
    
КонецФункции

Аутентификация и авторизация

JWT токены в 1С 8.3.21+

С версии 8.3.21.1302 платформа 1С получила нативную поддержку JWT-токенов. Это современный стандарт для API аутентификации:

// Создание JWT токена (модуль HTTP-сервиса)
Функция АутентификацияPOST(Запрос)
    
    Попытка
        ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
        ДанныеАвторизации = ДесериализоватьИзJSON(ТелоЗапроса);
        
        // Валидация входных данных
        Если НЕ ДанныеАвторизации.Свойство("username") ИЛИ 
           НЕ ДанныеАвторизации.Свойство("password") Тогда
            Возврат СформироватьОтветОбОшибке(400, "Отсутствуют учетные данные");
        КонецЕсли;
        
        // Проверка учетных данных
        Пользователь = ПроверитьУчетныеДанные(
            ДанныеАвторизации.username, 
            ДанныеАвторизации.password
        );
        
        Если Пользователь = Неопределено Тогда
            Возврат СформироватьОтветОбОшибке(401, "Неверные учетные данные");
        КонецЕсли;
        
        // Создание access токена
        ТокенДоступа = Новый ТокенДоступа;
        ТокенДоступа.Аудитория = "mobile-app";
        ТокенДоступа.Издатель = "1c-enterprise-api";
        ТокенДоступа.КлючСопоставленияПользователя = XMLСтрока(Пользователь);
        
        // Добавление дополнительных claims
        ТокенДоступа.Данные.Вставить("userId", XMLСтрока(Пользователь));
        ТокенДоступа.Данные.Вставить("username", ДанныеАвторизации.username);
        ТокенДоступа.Данные.Вставить("roles", ПолучитьРолиПользователя(Пользователь));
        ТокенДоступа.Данные.Вставить("permissions", ПолучитьПравыПользователя(Пользователь));
        
        // Время жизни токена
        ВремяВыдачи = ТекущаяУниверсальнаяДата();
        ВремяИстечения = ВремяВыдачи + 3600; // 1 час
        
        ТокенДоступа.ВременныеРамки.Добавить(ВремяВыдачи, ВремяИстечения);
        
        // Подписание токена
        КлючПодписи = ПолучитьКлючПодписиJWT(); // Должен быть минимум 256 бит
        ТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256, 
            ПолучитьДвоичныеДанныеИзСтроки(КлючПодписи));
        
        СтрокаТокена = ТокенДоступа.ПолучитьТокен();
        
        // Создание refresh токена
        РефрешТокен = СоздатьРефрешТокен(Пользователь);
        
        ОтветТокена = Новый Структура;
        ОтветТокена.Вставить("access_token", СтрокаТокена);
        ОтветТокена.Вставить("token_type", "Bearer");
        ОтветТокена.Вставить("expires_in", 3600);
        ОтветТокена.Вставить("refresh_token", РефрешТокен);
        ОтветТокена.Вставить("scope", "read write");
        
        // Логирование успешной аутентификации
        ЗаписьЖурналаРегистрации("JWT.Аутентификация", 
            УровеньЖурналаРегистрации.Информация,,,
            СтрШаблон("Успешная аутентификация пользователя %1", ДанныеАвторизации.username));
        
        Ответ = Новый HTTPСервисОтвет(200);
        Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
        Ответ.Заголовки.Вставить("Cache-Control", "no-store");
        Ответ.Заголовки.Вставить("Pragma", "no-cache");
        Ответ.УстановитьТелоИзСтроки(СериализоватьВJSON(ОтветТокена), 
            "UTF-8", ИспользованиеByteOrderMark.НеИспользовать);
            
        Возврат Ответ;
        
    Исключение
        Возврат СформироватьОтветОбОшибке(500, "Ошибка аутентификации", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Проверка JWT токена

Функция ПроверитьJWTТокен(Запрос)
    
    ЗаголовокАвторизации = Запрос.Заголовки.Получить("Authorization");
    
    Если ПустаяСтрока(ЗаголовокАвторизации) Тогда
        Возврат Неопределено;
    КонецЕсли;
    
    Если НЕ СтрНачинаетсяС(ВРег(ЗаголовокАвторизации), "BEARER ") Тогда
        Возврат Неопределено;
    КонецЕсли;
    
    СтрокаТокена = Сред(ЗаголовокАвторизации, 8);
    
    Попытка
        КлючПодписи = ПолучитьКлючПодписиJWT();
        
        ТокенДоступа = Новый ТокенДоступа;
        ТокенДоступа.УстановитьТокен(СтрокаТокена, 
            АлгоритмПодписиТокенаДоступа.HS256,
            ПолучитьДвоичныеДанныеИзСтроки(КлючПодписи));
        
        // Проверка валидности токена
        Если НЕ ТокенДоступа.Валиден Тогда
            ЗаписьЖурналаРегистрации("JWT.Ошибка", 
                УровеньЖурналаРегистрации.Предупреждение,,,
                "Получен невалидный JWT токен");
            Возврат Неопределено;
        КонецЕсли;
        
        // Проверка времени истечения
        ТекущееВремя = ТекущаяУниверсальнаяДата();
        ВременныеРамки = ТокенДоступа.ВременныеРамки;
        
        ТокенДействителен = Ложь;
        Для Каждого ВременнаяРамка Из ВременныеРамки Цикл
            Если ТекущееВремя >= ВременнаяРамка.НачалоДействия И 
               ТекущееВремя <= ВременнаяРамка.ОкончаниеДействия Тогда
                ТокенДействителен = Истина;
                Прервать;
            КонецЕсли;
        КонецЦикла;
        
        Если НЕ ТокенДействителен Тогда
            ЗаписьЖурналаРегистрации("JWT.Ошибка", 
                УровеньЖурналаРегистрации.Предупреждение,,,
                "JWT токен истек");
            Возврат Неопределено;
        КонецЕсли;
        
        // Получение пользователя
        КлючПользователя = ТокенДоступа.КлючСопоставленияПользователя;
        Пользователь = XMLЗначение(Тип("СправочникСсылка.Пользователи"), 
            КлючПользователя);
        
        // Проверка активности пользователя
        Если НЕ ЗначениеЗаполнено(Пользователь) ИЛИ Пользователь.ПометкаУдаления Тогда
            Возврат Неопределено;
        КонецЕсли;
        
        // Формирование контекста пользователя
        КонтекстПользователя = Новый Структура;
        КонтекстПользователя.Вставить("Пользователь", Пользователь);
        КонтекстПользователя.Вставить("Роли", ТокенДоступа.Данные.Получить("roles"));
        КонтекстПользователя.Вставить("Права", ТокенДоступа.Данные.Получить("permissions"));
        КонтекстПользователя.Вставить("ИмяПользователя", ТокенДоступа.Данные.Получить("username"));
        
        Возврат КонтекстПользователя;
        
    Исключение
        ЗаписьЖурналаРегистрации("JWT.Ошибка", 
            УровеньЖурналаРегистрации.Ошибка,,,
            СтрШаблон("Ошибка проверки JWT токена: %1", ОписаниеОшибки()));
        Возврат Неопределено;
    КонецПопытки;
    
КонецФункции

Обновление токена (Refresh)

Функция ОбновитьТокенPOST(Запрос)
    
    Попытка
        ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
        ДанныеОбновления = ДесериализоватьИзJSON(ТелоЗапроса);
        
        Если НЕ ДанныеОбновления.Свойство("refresh_token") Тогда
            Возврат СформироватьОтветОбОшибке(400, "Отсутствует refresh_token");
        КонецЕсли;
        
        РефрешТокен = ДанныеОбновления.refresh_token;
        
        // Проверка действительности refresh токена
        Пользователь = ПроверитьРефрешТокен(РефрешТокен);
        Если Пользователь = Неопределено Тогда
            Возврат СформироватьОтветОбОшибке(401, "Недействительный refresh_token");
        КонецЕсли;
        
        // Создание нового access токена
        НовыйТокенДоступа = Новый ТокенДоступа;
        НовыйТокенДоступа.Аудитория = "mobile-app";
        НовыйТокенДоступа.Издатель = "1c-enterprise-api";
        НовыйТокенДоступа.КлючСопоставленияПользователя = XMLСтрока(Пользователь);
        
        НовыйТокенДоступа.Данные.Вставить("userId", XMLСтрока(Пользователь));
        НовыйТокенДоступа.Данные.Вставить("roles", ПолучитьРолиПользователя(Пользователь));
        НовыйТокенДоступа.Данные.Вставить("permissions", ПолучитьПравыПользователя(Пользователь));
        
        ВремяВыдачи = ТекущаяУниверсальнаяДата();
        ВремяИстечения = ВремяВыдачи + 3600;
        НовыйТокенДоступа.ВременныеРамки.Добавить(ВремяВыдачи, ВремяИстечения);
        
        КлючПодписи = ПолучитьКлючПодписиJWT();
        НовыйТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256, 
            ПолучитьДвоичныеДанныеИзСтроки(КлючПодписи));
        
        СтрокаНовогоТокена = НовыйТокенДоступа.ПолучитьТокен();
        
        // Создание нового refresh токена (ротация токенов)
        НовыйРефрешТокен = СоздатьРефрешТокен(Пользователь);
        
        // Инвалидация старого refresh токена
        ИнвалидироватьРефрешТокен(РефрешТокен);
        
        ОтветТокена = Новый Структура;
        ОтветТокена.Вставить("access_token", СтрокаНовогоТокена);
        ОтветТокена.Вставить("token_type", "Bearer");
        ОтветТокена.Вставить("expires_in", 3600);
        ОтветТокена.Вставить("refresh_token", НовыйРефрешТокен);
        
        Ответ = Новый HTTPСервисОтвет(200);
        Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
        Ответ.УстановитьТелоИзСтроки(СериализоватьВJSON(ОтветТокена), 
            "UTF-8", ИспользованиеByteOrderMark.НеИспользовать);
            
        Возврат Ответ;
        
    Исключение
        Возврат СформироватьОтветОбОшибке(500, "Ошибка обновления токена", 
            ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Вспомогательные функции для JWT аутентификации

Приведенные выше примеры используют несколько вспомогательных функций, которые необходимо реализовать отдельно:

// === Управление Refresh токенами ===

Функция СоздатьРефрешТокен(Пользователь)
    // Создание уникального refresh токена
    РефрешТокен = СтрЗаменить(Строка(Новый УникальныйИдентификатор), "-", "");
    
    // Время жизни refresh токена (30 дней)
    ВремяИстечения = ТекущаяУниверсальнаяДата() + 30 * 24 * 3600;
    
    // Сохранение в базе данных
    НаборЗаписей = РегистрыСведений.RefreshТокены.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Токен.Установить(РефрешТокен);
    
    НоваяЗапись = НаборЗаписей.Добавить();
    НоваяЗапись.Токен = РефрешТокен;
    НоваяЗапись.Пользователь = Пользователь;
    НоваяЗапись.ДатаСоздания = ТекущаяУниверсальнаяДата();
    НоваяЗапись.ДатаИстечения = ВремяИстечения;
    НоваяЗапись.Активен = Истина;
    НоваяЗапись.IPАдрес = ПолучитьIPАдресКлиента();
    НоваяЗапись.UserAgent = ПолучитьUserAgent();
    
    НаборЗаписей.Записать();
    
    Возврат РефрешТокен;
КонецФункции

Функция ПроверитьРефрешТокен(РефрешТокен)
    Попытка
        Запрос = Новый Запрос;
        Запрос.Текст = 
        "ВЫБРАТЬ
        |    RefreshТокены.Пользователь КАК Пользователь,
        |    RefreshТокены.ДатаИстечения
        |ИЗ
        |    РегистрСведений.RefreshТокены КАК RefreshТокены
        |ГДЕ
        |    RefreshТокены.Токен = &Токен
        |    И RefreshТокены.Активен = ИСТИНА
        |    И RefreshТокены.ДатаИстечения > &ТекущаяДата";
        
        Запрос.УстановитьПараметр("Токен", РефрешТокен);
        Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяУниверсальнаяДата());
        
        Результат = Запрос.Выполнить();
        Если Результат.Пустой() Тогда
            Возврат Неопределено;
        КонецЕсли;
        
        Выборка = Результат.Выбрать();
        Выборка.Следующий();
        
        Возврат Выборка.Пользователь;
        
    Исключение
        ЗаписьЖурналаРегистрации("JWT.РефрешТокен", 
            УровеньЖурналаРегистрации.Ошибка,,,
            "Ошибка проверки refresh токена: " + ОписаниеОшибки());
        Возврат Неопределено;
    КонецПопытки;
КонецФункции

Процедура ИнвалидироватьРефрешТокен(РефрешТокен)
    Попытка
        НаборЗаписей = РегистрыСведений.RefreshТокены.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Токен.Установить(РефрешТокен);
        НаборЗаписей.Прочитать();
        
        Для Каждого Запись Из НаборЗаписей Цикл
            Запись.Активен = Ложь;
            Запись.ДатаИнвалидации = ТекущаяУниверсальнаяДата();
        КонецЦикла;
        
        НаборЗаписей.Записать();
    Исключение
        ЗаписьЖурналаРегистрации("JWT.РефрешТокен", 
            УровеньЖурналаРегистрации.Ошибка,,,
            "Ошибка инвалидации refresh токена: " + ОписаниеОшибки());
    КонецПопытки;
КонецПроцедуры

// === Вспомогательные функции безопасности ===

Функция ПолучитьКлючПодписиJWT()
    // В production среде ключ должен храниться в защищенном месте
    // Например, в константе с шифрованием или внешнем хранилище ключей
    
    КлючИзКонстанты = Константы.КлючПодписиJWT.Получить();
    Если НЕ ПустаяСтрока(КлючИзКонстанты) Тогда
        Возврат КлючИзКонстанты;
    КонецЕсли;
    
    // Генерация нового ключа при первом запуске
    НовыйКлюч = "";
    Для Сч = 1 По 64 Цикл
        НовыйКлюч = НовыйКлюч + Символ(Окр(СлучайноеЧисло() * 255));
    КонецЦикла;
    
    Константы.КлючПодписиJWT.Установить(НовыйКлюч);
    
    Возврат НовыйКлюч;
КонецФункции

Функция ПолучитьРолиПользователя(Пользователь)
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    РолиПользователей.Роль.Наименование КАК Роль
    |ИЗ
    |    РегистрСведений.РолиПользователей КАК РолиПользователей
    |ГДЕ
    |    РолиПользователей.Пользователь = &Пользователь
    |    И РолиПользователей.Активен = ИСТИНА";
    
    Запрос.УстановитьПараметр("Пользователь", Пользователь);
    
    Результат = Запрос.Выполнить();
    Выборка = Результат.Выбрать();
    
    Роли = Новый Массив;
    Пока Выборка.Следующий() Цикл
        Роли.Добавить(Выборка.Роль);
    КонецЦикла;
    
    Возврат Роли;
КонецФункции

Функция ПолучитьПравыПользователя(Пользователь)
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    ПравыПользователей.Право,
    |    ПравыПользователей.Объект
    |ИЗ
    |    РегистрСведений.ПравыПользователей КАК ПравыПользователей
    |ГДЕ
    |    ПравыПользователей.Пользователь = &Пользователь
    |    И ПравыПользователей.Разрешено = ИСТИНА";
    
    Запрос.УстановитьПараметр("Пользователь", Пользователь);
    
    Результат = Запрос.Выполнить();
    Выборка = Результат.Выбрать();
    
    Права = Новый Структура;
    Пока Выборка.Следующий() Цикл
        Если НЕ Права.Свойство(Выборка.Право) Тогда
            Права.Вставить(Выборка.Право, Новый Массив);
        КонецЕсли;
        Права[Выборка.Право].Добавить(Выборка.Объект);
    КонецЦикла;
    
    Возврат Права;
КонецФункции

Функция ПолучитьIPАдресКлиента()
    // Получение IP адреса из заголовков HTTP запроса
    Попытка
        ИнформацияОСеансе = ПолучитьИнформациюЭкземпляраИнформационнойБазы().АктивныеСоединения[0];
        Возврат ИнформацияОСеансе.АдресСоединения;
    Исключение
        Возврат "unknown";
    КонецПопытки;
КонецФункции

Функция ПолучитьUserAgent()
    // В контексте HTTP-сервиса можно получить из заголовков запроса
    Попытка
        Возврат ТекущийHTTPЗапрос.Заголовки.Получить("User-Agent");
    Исключение
        Возврат "unknown";
    КонецПопытки;
КонецФункции

Общие вспомогательные функции

Для корректной работы всех приведенных примеров необходимы следующие общие функции:

// === Обработка ошибок ===

Функция СформироватьОтветОбОшибке(КодСостояния, Сообщение, ПодробностиОшибки = "")
    СтруктураОшибки = Новый Структура;
    СтруктураОшибки.Вставить("error", Истина);
    СтруктураОшибки.Вставить("message", Сообщение);
    СтруктураОшибки.Вставить("timestamp", ТекущаяУниверсальнаяДата());
    СтруктураОшибки.Вставить("code", КодСостояния);
    
    Если НЕ ПустаяСтрока(ПодробностиОшибки) Тогда
        СтруктураОшибки.Вставить("details", ПодробностиОшибки);
    КонецЕсли;
    
    // Логирование ошибки
    ЗаписьЖурналаРегистрации("API.Ошибка", 
        УровеньЖурналаРегистрации.Ошибка,,,
        СтрШаблон("HTTP %1: %2. Детали: %3", КодСостояния, Сообщение, ПодробностиОшибки));
    
    Ответ = Новый HTTPСервисОтвет(КодСостояния);
    Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
    Ответ.УстановитьТелоИзСтроки(СериализоватьВJSON(СтруктураОшибки), 
        "UTF-8", ИспользованиеByteOrderMark.НеИспользовать);
        
    Возврат Ответ;
КонецФункции

// === Валидация JSON ===

Функция ВалидироватьJSONПоСхеме(СтрокаJSON, СхемаВалидации) Экспорт
    // Простая реализация валидации JSON по схеме
    Попытка
        ДанныеJSON = ДесериализоватьИзJSON(СтрокаJSON);
        
        РезультатВалидации = Новый Структура;
        РезультатВалидации.Вставить("Валидно", Истина);
        РезультатВалидации.Вставить("Ошибки", Новый Массив);
        
        // Проверка обязательных полей
        Если СхемаВалидации.Свойство("required") Тогда
            Для Каждого ОбязательноеПоле Из СхемаВалидации.required Цикл
                Если НЕ ДанныеJSON.Свойство(ОбязательноеПоле) Тогда
                    РезультатВалидации.Валидно = Ложь;
                    РезультатВалидации.Ошибки.Добавить(
                        СтрШаблон("Отсутствует обязательное поле: %1", ОбязательноеПоле));
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        // Проверка типов данных
        Если СхемаВалидации.Свойство("properties") Тогда
            Для Каждого КлючЗначение Из СхемаВалидации.properties Цикл
                ИмяПоля = КлючЗначение.Ключ;
                ОписаниеПоля = КлючЗначение.Значение;
                
                Если ДанныеJSON.Свойство(ИмяПоля) Тогда
                    ЗначениеПоля = ДанныеJSON[ИмяПоля];
                    
                    Если ОписаниеПоля.Свойство("type") Тогда
                        ОжидаемыйТип = ОписаниеПоля.type;
                        ФактическийТип = ОпределитьТипJSON(ЗначениеПоля);
                        
                        Если ОжидаемыйТип <> ФактическийТип Тогда
                            РезультатВалидации.Валидно = Ложь;
                            РезультатВалидации.Ошибки.Добавить(
                                СтрШаблон("Неверный тип поля %1: ожидался %2, получен %3", 
                                    ИмяПоля, ОжидаемыйТип, ФактическийТип));
                        КонецЕсли;
                    КонецЕсли;
                    
                    // Проверка длины строки
                    Если ОписаниеПоля.Свойство("maxLength") И ТипЗнч(ЗначениеПоля) = Тип("Строка") Тогда
                        Если СтрДлина(ЗначениеПоля) > ОписаниеПоля.maxLength Тогда
                            РезультатВалидации.Валидно = Ложь;
                            РезультатВалидации.Ошибки.Добавить(
                                СтрШаблон("Поле %1 превышает максимальную длину %2", 
                                    ИмяПоля, ОписаниеПоля.maxLength));
                        КонецЕсли;
                    КонецЕсли;
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        Возврат РезультатВалидации;
        
    Исключение
        РезультатВалидации = Новый Структура;
        РезультатВалидации.Вставить("Валидно", Ложь);
        РезультатВалидации.Вставить("Ошибки", Новый Массив);
        РезультатВалидации.Ошибки.Добавить("Ошибка парсинга JSON: " + ОписаниеОшибки());
        
        Возврат РезультатВалидации;
    КонецПопытки;
КонецФункции

Функция ОпределитьТипJSON(Значение)
    ТипЗначения = ТипЗнч(Значение);
    
    Если ТипЗначения = Тип("Строка") Тогда
        Возврат "string";
    ИначеЕсли ТипЗначения = Тип("Число") Тогда
        Возврат "number";
    ИначеЕсли ТипЗначения = Тип("Булево") Тогда
        Возврат "boolean";
    ИначеЕсли ТипЗначения = Тип("Массив") Тогда
        Возврат "array";
    ИначеЕсли ТипЗначения = Тип("Структура") ИЛИ ТипЗначения = Тип("Соответствие") Тогда
        Возврат "object";
    ИначеЕсли Значение = Неопределено Тогда
        Возврат "null";
    Иначе
        Возврат "unknown";
    КонецЕсли;
КонецФункции

// === Аутентификация пользователей ===

Функция ПроверитьПодлинностьПользователя(ИмяПользователя, Пароль)
    Попытка
        Запрос = Новый Запрос;
        Запрос.Текст = 
        "ВЫБРАТЬ
        |    Пользователи.Ссылка КАК Пользователь,
        |    Пользователи.ХешПароля,
        |    Пользователи.Соль,
        |    Пользователи.Активен,
        |    Пользователи.КоличествоНеудачныхВходов,
        |    Пользователи.ВременнаяБлокировка
        |ИЗ
        |    Справочник.Пользователи КАК Пользователи
        |ГДЕ
        |    Пользователи.ИмяПользователя = &ИмяПользователя
        |    И НЕ Пользователи.ПометкаУдаления";
        
        Запрос.УстановитьПараметр("ИмяПользователя", ИмяПользователя);
        
        Результат = Запрос.Выполнить();
        Если Результат.Пустой() Тогда
            Возврат Неопределено;
        КонецЕсли;
        
        Выборка = Результат.Выбрать();
        Выборка.Следующий();
        
        // Проверка блокировки
        Если НЕ Выборка.Активен ИЛИ Выборка.ВременнаяБлокировка > ТекущаяУниверсальнаяДата() Тогда
            Возврат Неопределено;
        КонецЕсли;
        
        // Проверка пароля
        ХешВведенногоПароля = ХешироватьПароль(Пароль, Выборка.Соль);
        Если ХешВведенногоПароля = Выборка.ХешПароля Тогда
            // Сброс счетчика неудачных попыток
            СбросСчетчикаНеудачныхВходов(Выборка.Пользователь);
            Возврат Выборка.Пользователь;
        Иначе
            // Увеличение счетчика неудачных попыток
            УвеличитьСчетчикНеудачныхВходов(Выборка.Пользователь);
            Возврат Неопределено;
        КонецЕсли;
        
    Исключение
        ЗаписьЖурналаРегистрации("API.Аутентификация", 
            УровеньЖурналаРегистрации.Ошибка,,,
            "Ошибка аутентификации: " + ОписаниеОшибки());
        Возврат Неопределено;
    КонецПопытки;
КонецФункции

Функция ХешироватьПароль(Пароль, Соль)
    // Используем SHA-256 с солью для хеширования пароля
    ХешированиеДанных = Новый ХешированиеДанных(ХешФункция.SHA256);
    ХешированиеДанных.Добавить(ПолучитьДвоичныеДанныеИзСтроки(Соль + Пароль, "UTF-8"));
    Возврат Base64Строка(ХешированиеДанных.ХешСумма);
КонецФункции

Процедура СбросСчетчикаНеудачныхВходов(Пользователь)
    НачатьТранзакцию();
    Попытка
        СправочникОбъект = Пользователь.ПолучитьОбъект();
        СправочникОбъект.КоличествоНеудачныхВходов = 0;
        СправочникОбъект.ВременнаяБлокировка = Дата(1, 1, 1);
        СправочникОбъект.Записать();
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
    КонецПопытки;
КонецПроцедуры

Процедура УвеличитьСчетчикНеудачныхВходов(Пользователь)
    НачатьТранзакцию();
    Попытка
        СправочникОбъект = Пользователь.ПолучитьОбъект();
        СправочникОбъект.КоличествоНеудачныхВходов = СправочникОбъект.КоличествоНеудачныхВходов + 1;
        
        // Блокировка после 5 неудачных попыток на 15 минут
        Если СправочникОбъект.КоличествоНеудачныхВходов >= 5 Тогда
            СправочникОбъект.ВременнаяБлокировка = ТекущаяУниверсальнаяДата() + 15 * 60; // 15 минут
        КонецЕсли;
        
        СправочникОбъект.Записать();
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
    КонецПопытки;
КонецПроцедуры

// === Функции кеширования ===

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

Процедура ПоместитьВКеш(КлючКэша, Данные, ВремяЖизниВСекундах = 3600, КатегорияКэша = "Общий")
    Попытка
        КэшированныеДанные = Новый Структура;
        КэшированныеДанные.Вставить("Данные", Данные);
        КэшированныеДанные.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
        КэшированныеДанные.Вставить("ВремяИстечения", 
            ТекущаяУниверсальнаяДата() + ВремяЖизниВСекундах);
        
        ХранилищеОбщихНастроек.Сохранить(КатегорияКэша, КлючКэша, КэшированныеДанные);
        
        ЗаписьЖурналаРегистрации("Кэширование", 
            УровеньЖурналаРегистрации.Информация,,,
            СтрШаблон("Данные помещены в кэш: %1 (время жизни: %2 сек)", 
                КлючКэша, ВремяЖизниВСекундах));
        
    Исключение
        ЗаписьЖурналаРегистрации("Кэширование", 
            УровеньЖурналаРегистрации.Ошибка,,,
            СтрШаблон("Ошибка помещения в кэш: %1", ОписаниеОшибки()));
    КонецПопытки;
КонецПроцедуры

Процедура ОчиститьКеш(КатегорияКэша = "Общий", КлючМаска = "")
    Попытки
        Если ПустаяСтрока(КлючМаска) Тогда
            // Очистка всей категории
            ХранилищеОбщихНастроек.Удалить(КатегорияКэша);
        Иначе
            // Получение всех ключей и удаление по маске
            ВсеКлючи = ХранилищеОбщихНастроек.ПолучитьСписок(КатегорияКэша);
            Для Каждого КлючЗначение Из ВсеКлючи Цикл
                Если СтрНайти(КлючЗначение.Ключ, КлючМаска) > 0 Тогда
                    ХранилищеОбщихНастроек.Удалить(КатегорияКэша, КлючЗначение.Ключ);
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
        
        ЗаписьЖурналаРегистрации("Кэширование", 
            УровеньЖурналаРегистрации.Информация,,,
            СтрШаблон("Очищен кэш категории '%1' по маске '%2'", КатегорияКэша, КлючМаска));
        
    Исключение
        ЗаписьЖурналаРегистрации("Кэширование", 
            УровеньЖурналаРегистрации.Ошибка,,,
            СтрШаблон("Ошибка очистки кэша: %1", ОписаниеОшибки()));
    КонецПопытки;
КонецПроцедуры

Basic Authentication для простых случаев

Функция ПроверитьBasicAuth(Запрос)
    
    ЗаголовокАвторизации = Запрос.Заголовки.Получить("Authorization");
    
    Если ПустаяСтрока(ЗаголовокАвторизации) Тогда
        Возврат Неопределено;
    КонецЕсли;
    
    Если НЕ СтрНачинаетсяС(ВРег(ЗаголовокАвторизации), "BASIC ") Тогда
        Возврат Неопределено;
    КонецЕсли;
    
    Попытка
        КодированныеДанные = Сред(ЗаголовокАвторизации, 7);
        ДвоичныеДанные = Base64Значение(КодированныеДанные);
        СтрокаАвторизации = ПолучитьСтрокуИзДвоичныхДанных(ДвоичныеДанные);
        
        ПозицияДвоеточия = СтрНайти(СтрокаАвторизации, ":");
        Если ПозицияДвоеточия = 0 Тогда
            Возврат Неопределено;
        КонецЕсли;
        
        Логин = Лев(СтрокаАвторизации, ПозицияДвоеточия - 1);
        Пароль = Сред(СтрокаАвторизации, ПозицияДвоеточия + 1);
        
        // Проверка на пустые значения
        Если ПустаяСтрока(Логин) ИЛИ ПустаяСтрока(Пароль) Тогда
            Возврат Неопределено;
        КонецЕсли;
        
        Возврат ПроверитьУчетныеДанные(Логин, Пароль);
        
    Исключение
        ЗаписьЖурналаРегистрации("BasicAuth.Ошибка", 
            УровеньЖурналаРегистрации.Предупреждение,,,
            СтрШаблон("Ошибка декодирования Basic Auth: %1", ОписаниеОшибки()));
        Возврат Неопределено;
    КонецПопытки;
    
КонецФункции

API Key аутентификация

Функция ПроверитьAPIKey(Запрос)
    
    // Проверяем заголовок X-API-Key
    APIKey = Запрос.Заголовки.Получить("X-API-Key");
    
    // Альтернативно - параметр запроса
    Если ПустаяСтрока(APIKey) Тогда
        APIKey = Запрос.ПараметрыЗапроса.Получить("api_key");
    КонецЕсли;
    
    Если ПустаяСтрока(APIKey) Тогда
        Возврат Неопределено;
    КонецЕсли;
    
    // Поиск пользователя по API ключу
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    Пользователи.Ссылка КАК Пользователь,
    |    Пользователи.Наименование,
    |    APIКлючи.Права,
    |    APIКлючи.ДатаИстечения,
    |    APIКлючи.КоличествоЗапросов,
    |    APIКлючи.МаксимумЗапросовВДень
    |ИЗ
    |    РегистрСведений.APIКлючи КАК APIКлючи
    |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Пользователи КАК Пользователи
    |        ПО APIКлючи.Пользователь = Пользователи.Ссылка
    |ГДЕ
    |    APIКлючи.Ключ = &APIKey
    |    И APIКлючи.Активен
    |    И НЕ Пользователи.ПометкаУдаления
    |    И (APIКлючи.ДатаИстечения = ДАТАВРЕМЯ(1, 1, 1) 
    |         ИЛИ APIКлючи.ДатаИстечения > &ТекущаяДата)";
    
    Запрос.УстановитьПараметр("APIKey", APIKey);
    Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    Если НЕ Выборка.Следующий() Тогда
        ЗаписьЖурналаРегистрации("APIKey.Ошибка", 
            УровеньЖурналаРегистрации.Предупреждение,,,
            СтрШаблон("Недействительный API ключ: %1", Лев(APIKey, 8) + "..."));
        Возврат Неопределено;
    КонецЕсли;
    
    // Проверка лимитов запросов
    Если Выборка.МаксимумЗапросовВДень > 0 И 
       Выборка.КоличествоЗапросов >= Выборка.МаксимумЗапросовВДень Тогда
        ЗаписьЖурналаРегистрации("APIKey.Лимит", 
            УровеньЖурналаРегистрации.Предупреждение,,,
            СтрШаблон("Превышен лимит запросов для API ключа пользователя %1", 
                Выборка.Наименование));
        Возврат "RATE_LIMIT_EXCEEDED";
    КонецЕсли;
    
    // Увеличение счетчика запросов
    УвеличитьСчетчикЗапросовAPIKey(APIKey);
    
    КонтекстПользователя = Новый Структура;
    КонтекстПользователя.Вставить("Пользователь", Выборка.Пользователь);
    КонтекстПользователя.Вставить("Права", Выборка.Права);
    КонтекстПользователя.Вставить("ТипАутентификации", "APIKey");
    
    Возврат КонтекстПользователя;
    
КонецФункции

Обработка ошибок и диагностика

Комплексная система обработки HTTP-ошибок

// Общий модуль ОбработкаОшибокHTTP

Функция ВыполнитьHTTPЗапросСПовторами(Соединение, Запрос, МаксимумПовторов = 3) Экспорт
    
    ДанныеОперации = Новый Структура;
    ДанныеОперации.Вставить("НачалоВыполнения", ТекущаяУниверсальнаяДата());
    ДанныеОперации.Вставить("URLЗапроса", Запрос.АдресРесурса);
    ДанныеОперации.Вставить("МетодHTTP", ОпределитьМетодHTTP(Соединение, Запрос));
    ДанныеОперации.Вставить("КоличествоПопыток", 0);
    
    Для НомерПопытки = 1 По МаксимумПовторов Цикл
        
        ДанныеОперации.КоличествоПопыток = НомерПопытки;
        
        Попытка
            // Определяем метод и выполняем запрос
            Если ДанныеОперации.МетодHTTP = "GET" Тогда
                Ответ = Соединение.Получить(Запрос);
            ИначеЕсли ДанныеОперации.МетодHTTP = "POST" Тогда
                Ответ = Соединение.ОтправитьДляОбработки(Запрос);
            ИначеЕсли ДанныеОперации.МетодHTTP = "PUT" Тогда
                Ответ = Соединение.Записать(Запрос);
            ИначеЕсли ДанныеОперации.МетодHTTP = "DELETE" Тогда
                Ответ = Соединение.Удалить(Запрос);
            КонецЕсли;
            
            // Анализ кода ответа
            Если Ответ.КодСостояния >= 200 И Ответ.КодСостояния < 300 Тогда
                // Успешный запрос
                ДанныеОперации.Вставить("РезультатОперации", "Успех");
                ДанныеОперации.Вставить("КодОтвета", Ответ.КодСостояния);
                ДанныеОперации.Вставить("ВремяВыполнения", 
                    (ТекущаяУниверсальнаяДата() - ДанныеОперации.НачалоВыполнения) * 1000); // мс
                ДанныеОперации.Вставить("РазмерОтвета", СтрДлина(Ответ.ПолучитьТелоКакСтроку()));
                    
                ЗаписатьЛогОперации(ДанныеОперации);
                Возврат Ответ;
                
            ИначеЕсли Ответ.КодСостояния >= 500 И НомерПопытки < МаксимумПовторов Тогда
                // Ошибка сервера - попробуем повторить
                ВремяЗадержки = РассчитатьВремяЗадержки(НомерПопытки, Ответ);
                
                ЗаписьЖурналаРегистрации("HTTP.ПовторныйЗапрос", 
                    УровеньЖурналаРегистрации.Предупреждение,,,
                    СтрШаблон("Повтор %1/%2 через %3 сек. Код: %4, URL: %5", 
                        НомерПопытки, МаксимумПовторов, ВремяЗадержки, 
                        Ответ.КодСостояния, Запрос.АдресРесурса));
                
                Приостановить(ВремяЗадержки * 1000); // миллисекунды
                Продолжить;
                
            ИначеЕсли Ответ.КодСостояния >= 400 И Ответ.КодСостояния < 500 Тогда
                // Ошибка клиента - повторять нет смысла
                ДанныеОперации.Вставить("РезультатОперации", "ОшибкаКлиента");
                ДанныеОперации.Вставить("КодОтвета", Ответ.КодСостояния);
                ЗаписатьЛогОперации(ДанныеОперации);
                
                ИсключениеHTTP = СформироватьИсключениеHTTP(Ответ);
                ВызватьИсключение ИсключениеHTTP;
                
            КонецЕсли;
            
        Исключение
            Если НомерПопытки = МаксимумПовторов Тогда
                ДанныеОперации.Вставить("РезультатОперации", "ОшибкаСоединения");
                ДанныеОперации.Вставить("ТекстОшибки", ОписаниеОшибки());
                ЗаписатьЛогОперации(ДанныеОперации);
                ВызватьИсключение;
            КонецЕсли;
            
            ЗаписьЖурналаРегистрации("HTTP.ОшибкаСоединения", 
                УровеньЖурналаРегистрации.Предупреждение,,,
                СтрШаблон("Попытка %1/%2 для %3: %4", 
                    НомерПопытки, МаксимумПовторов, Запрос.АдресРесурса, ОписаниеОшибки()));
                    
            // Экспоненциальная задержка при ошибке соединения
            ВремяЗадержки = Степень(2, НомерПопытки - 1); // 1, 2, 4, 8...
            Приостановить(ВремяЗадержки * 1000);
        КонецПопытки;
        
    КонецЦикла;
    
КонецФункции

Функция РассчитатьВремяЗадержки(НомерПопытки, Ответ)
    
    // Проверяем заголовок Retry-After
    RetryAfter = Ответ.Заголовки.Получить("Retry-After");
    Если НЕ ПустаяСтрока(RetryAfter) Тогда
        ЧисловоеЗначение = 0;
        Попытка
            ЧисловоеЗначение = Число(RetryAfter);
            Если ЧисловоеЗначение > 0 И ЧисловоеЗначение <= 300 Тогда // Максимум 5 минут
                Возврат ЧисловоеЗначение;
            КонецЕсли;
        Исключение
            // Возможно, значение в формате даты HTTP, игнорируем
        КонецПопытки;
    КонецЕсли;
    
    // Экспоненциальная задержка с джиттером
    БазоваяЗадержка = Степень(2, НомерПопытки - 1); // 1, 2, 4, 8...
    МаксимальнаяЗадержка = 60; // Не больше минуты
    
    БазоваяЗадержка = Мин(БазоваяЗадержка, МаксимальнаяЗадержка);
    
    // Добавляем случайный джиттер ±25%
    Джиттер = Цел(Случайное() * БазоваяЗадержка * 0.5) - БазоваяЗадержка * 0.25;
    
    ИтоговаяЗадержка = БазоваяЗадержка + Джиттер;
    
    Возврат Макс(1, ИтоговаяЗадержка); // Минимум 1 секунда
    
КонецФункции

Функция СформироватьИсключениеHTTP(Ответ)
    
    ТипОшибки = "";
    РекомендацииПользователю = "";
    ДетальноеОписание = "";
    
    Если Ответ.КодСостояния = 400 Тогда
        ТипОшибки = "Некорректный запрос";
        РекомендацииПользователю = "Проверьте правильность передаваемых данных";
    ИначеЕсли Ответ.КодСостояния = 401 Тогда
        ТипОшибки = "Требуется аутентификация";
        РекомендацииПользователю = "Проверьте учетные данные или токен доступа";
    ИначеЕсли Ответ.КодСостояния = 403 Тогда
        ТипОшибки = "Доступ запрещен";
        РекомендацииПользователю = "Недостаточно прав для выполнения операции";
    ИначеЕсли Ответ.КодСостояния = 404 Тогда
        ТипОшибки = "Ресурс не найден";
        РекомендацииПользователю = "Проверьте правильность URL и существование ресурса";
    ИначеЕсли Ответ.КодСостояния = 409 Тогда
        ТипОшибки = "Конфликт данных";
        РекомендацииПользователю = "Ресурс уже существует или изменен другим пользователем";
    ИначеЕсли Ответ.КодСостояния = 422 Тогда
        ТипОшибки = "Ошибка валидации";
        РекомендацииПользователю = "Проверьте корректность формата данных";
    ИначеЕсли Ответ.КодСостояния = 429 Тогда
        ТипОшибки = "Превышен лимит запросов";
        РекомендацииПользователю = "Повторите попытку позже";
        
        // Извлекаем время ожидания из заголовка
        RetryAfter = Ответ.Заголовки.Получить("Retry-After");
        Если НЕ ПустаяСтрока(RetryAfter) Тогда
            РекомендацииПользователю = РекомендацииПользователю + 
                СтрШаблон(" (рекомендуемое время ожидания: %1 сек)", RetryAfter);
        КонецЕсли;
        
    ИначеЕсли Ответ.КодСостояния >= 500 Тогда
        ТипОшибки = "Ошибка сервера";
        РекомендацииПользователю = "Временная проблема на сервере, повторите попытку позже";
    Иначе
        ТипОшибки = СтрШаблон("HTTP ошибка %1", Ответ.КодСостояния);
        РекомендацииПользователю = "Обратитесь к администратору системы";
    КонецЕсли;
    
    // Попытка извлечь детали ошибки из тела ответа
    Попытка
        ТелоОтвета = Ответ.ПолучитьТелоКакСтроку("UTF-8");
        Если НЕ ПустаяСтрока(ТелоОтвета) И СтрДлина(ТелоОтвета) < 1000 Тогда
            // Проверяем, является ли ответ JSON
            Если СтрНачинаетсяС(Ответ.Заголовки.Получить("Content-Type"), "application/json") Тогда
                ДанныеОшибки = ДесериализоватьИзJSON(ТелоОтвета);
                Если ДанныеОшибки.Свойство("message") Тогда
                    ДетальноеОписание = ДанныеОшибки.message;
                ИначеЕсли ДанныеОшибки.Свойство("error") Тогда
                    ДетальноеОписание = ДанныеОшибки.error;
                КонецЕсли;
            Иначе
                ДетальноеОписание = ТелоОтвета;
            КонецЕсли;
        КонецЕсли;
    Исключение
        // Игнорируем ошибки парсинга тела ответа
    КонецПопытки;
    
    СообщениеОбОшибке = СтрШаблон("%1: %2. %3", 
        ТипОшибки, 
        Ответ.СтатусТекст,
        РекомендацииПользователю);
    
    Если НЕ ПустаяСтрока(ДетальноеОписание) Тогда
        СообщениеОбОшибке = СообщениеОбОшибке + Символы.ПС + "Детали: " + ДетальноеОписание;
    КонецЕсли;
    
    Возврат СообщениеОбОшибке;
    
КонецФункции

Логирование операций и мониторинг

Процедура ЗаписатьЛогОперации(ДанныеОперации) Экспорт
    
    // Подготовка данных для журнала
    СтруктураЛога = Новый Структура;
    СтруктураЛога.Вставить("timestamp", XMLСтрока(ДанныеОперации.НачалоВыполнения));
    СтруктураЛога.Вставить("method", ДанныеОперации.МетодHTTP);
    СтруктураЛога.Вставить("url", ДанныеОперации.URLЗапроса);
    СтруктураЛога.Вставить("result", ДанныеОперации.РезультатОперации);
    СтруктураЛога.Вставить("attempts", ДанныеОперации.КоличествоПопыток);
    
    Если ДанныеОперации.Свойство("КодОтвета") Тогда
        СтруктураЛога.Вставить("statusCode", ДанныеОперации.КодОтвета);
    КонецЕсли;
    
    Если ДанныеОперации.Свойство("ВремяВыполнения") Тогда
        СтруктураЛога.Вставить("durationMs", ДанныеОперации.ВремяВыполнения);
    КонецЕсли;
    
    Если ДанныеОперации.Свойство("РазмерОтвета") Тогда
        СтруктураЛога.Вставить("responseSize", ДанныеОперации.РазмерОтвета);
    КонецЕсли;
    
    Если ДанныеОперации.Свойство("ТекстОшибки") Тогда
        СтруктураЛога.Вставить("error", ДанныеОперации.ТекстОшибки);
    КонецЕсли;
    
    СтрокаЛога = СериализоватьВJSON(СтруктураЛога);
    
    // Определение уровня логирования
    УровеньЛога = УровеньЖурналаРегистрации.Информация;
    Если ДанныеОперации.РезультатОперации = "Успех" Тогда
        УровеньЛога = УровеньЖурналаРегистрации.Информация;
    ИначеЕсли ДанныеОперации.РезультатОперации = "ОшибкаКлиента" Тогда
        УровеньЛога = УровеньЖурналаРегистрации.Предупреждение;
    Иначе
        УровеньЛога = УровеньЖурналаРегистрации.Ошибка;
    КонецЕсли;
    
    ЗаписьЖурналаРегистрации("HTTP.Операция", УровеньЛога,,,СтрокаЛога);
    
    // Отправка метрик (неблокирующая)
    Попытка
        ОтправитьМетрикиHTTP(ДанныеОперации);
    Исключение
        // Ошибки метрик не должны влиять на основную логику
    КонецПопытки;
    
КонецПроцедуры

Процедура ОтправитьМетрикиHTTP(ДанныеОперации)
    
    // Счетчик запросов по методам
    ОтправитьМетрикиAPI("http_requests_total", 1, 
        Новый Структура("method", ДанныеОперации.МетодHTTP; 
                        "result", ДанныеОперации.РезультатОперации));
    
    // Время выполнения
    Если ДанныеОперации.Свойство("ВремяВыполнения") Тогда
        ОтправитьМетрикиAPI("http_request_duration_ms", ДанныеОперации.ВремяВыполнения,
            Новый Структура("method", ДанныеОперации.МетодHTTP));
    КонецЕсли;
    
    // Размер ответа
    Если ДанныеОперации.Свойство("РазмерОтвета") Тогда
        ОтправитьМетрикиAPI("http_response_size_bytes", ДанныеОперации.РазмерОтвета,
            Новый Структура("method", ДанныеОперации.МетодHTTP));
    КонецЕсли;
    
    // Количество попыток
    ОтправитьМетрикиAPI("http_request_attempts", ДанныеОперации.КоличествоПопыток,
        Новый Структура("method", ДанныеОперации.МетодHTTP));
    
КонецПроцедуры

Отладка HTTP-сервисов

Для эффективной отладки HTTP-сервисов создадим универсальную систему логирования:

// Функция для детального логирования HTTP-запросов (модуль HTTP-сервиса)
Функция ЛогироватьВходящийЗапрос(Запрос) Экспорт
    
    СтруктураЛога = Новый Структура;
    СтруктураЛога.Вставить("timestamp", XMLСтрока(ТекущаяУниверсальнаяДата()));
    СтруктураЛога.Вставить("method", Запрос.HTTPМетод);
    СтруктураЛога.Вставить("url", Запрос.ОтносительныйURL);
    СтруктураЛога.Вставить("remoteAddr", Запрос.АдресИсточника);
    СтруктураЛога.Вставить("userAgent", Запрос.Заголовки.Получить("User-Agent"));
    СтруктураЛога.Вставить("contentType", Запрос.Заголовки.Получить("Content-Type"));
    СтруктураЛога.Вставить("contentLength", Число(Запрос.Заголовки.Получить("Content-Length")));
    
    // Логирование параметров запроса
    Если Запрос.ПараметрыЗапроса.Количество() > 0 Тогда
        СтруктураЛога.Вставить("queryParams", Новый Структура);
        Для Каждого Параметр Из Запрос.ПараметрыЗапроса Цикл
            СтруктураЛога.queryParams.Вставить(Параметр.Ключ, Параметр.Значение);
        КонецЦикла;
    КонецЕсли;
    
    // Логирование заголовков (кроме конфиденциальных)
    СтруктураЛога.Вставить("headers", Новый Структура);
    КонфиденциальныеЗаголовки = Новый Массив;
    КонфиденциальныеЗаголовки.Добавить("AUTHORIZATION");
    КонфиденциальныеЗаголовки.Добавить("X-API-KEY");
    КонфиденциальныеЗаголовки.Добавить("COOKIE");
    
    Для Каждого Заголовок Из Запрос.Заголовки Цикл
        Если КонфиденциальныеЗаголовки.Найти(ВРег(Заголовок.Ключ)) <> Неопределено Тогда
            СтруктураЛога.headers.Вставить(Заголовок.Ключ, "[СКРЫТО]");
        Иначе
            СтруктураЛога.headers.Вставить(Заголовок.Ключ, Заголовок.Значение);
        КонецЕсли;
    КонецЦикла;
    
    // Логирование тела запроса (с ограничениями)
    Если Запрос.HTTPМетод = "POST" ИЛИ Запрос.HTTPМетод = "PUT" ИЛИ Запрос.HTTPМетод = "PATCH" Тогда
        ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
        Если СтрДлина(ТелоЗапроса) <= 2000 Тогда // Ограничение размера лога
            СтруктураЛога.Вставить("body", ТелоЗапроса);
        Иначе
            СтруктураЛога.Вставить("body", СтрШаблон("[БОЛЬШОЕ ТЕЛО ЗАПРОСА: %1 символов]", 
                СтрДлина(ТелоЗапроса)));
        КонецЕсли;
    КонецЕсли;
    
    СтрокаЛога = СериализоватьВJSON(СтруктураЛога);
    
    ЗаписьЖурналаРегистрации("HTTP.ВходящийЗапрос", 
        УровеньЖурналаРегистрации.Информация,,,
        СтрокаЛога);
    
КонецФункции

// Логирование исходящих ответов
Функция ЛогироватьИсходящийОтвет(Ответ, ВремяОбработки = 0) Экспорт
    
    СтруктураЛога = Новый Структура;
    СтруктураЛога.Вставить("timestamp", XMLСтрока(ТекущаяУниверсальнаяДата()));
    СтруктураЛога.Вставить("statusCode", Ответ.КодСостояния);
    СтруктураЛога.Вставить("statusText", Ответ.ПолучитьСтатусТекст());
    СтруктураЛога.Вставить("processingTimeMs", ВремяОбработки);
    
    // Логирование заголовков ответа
    СтруктураЛога.Вставить("responseHeaders", Новый Структура);
    Для Каждого Заголовок Из Ответ.Заголовки Цикл
        СтруктураЛога.responseHeaders.Вставить(Заголовок.Ключ, Заголовок.Значение);
    КонецЦикла;
    
    // Размер тела ответа
    РазмерТела = 0;
    Попытка
        ТелоОтвета = Ответ.ПолучитьТелоКакСтроку("UTF-8");
        РазмерТела = СтрДлина(ТелоОтвета);
        СтруктураЛога.Вставить("responseSize", РазмерТела);
        
        // Логируем только малые ответы или ошибки
        Если РазмерТела <= 1000 ИЛИ Ответ.КодСостояния >= 400 Тогда
            СтруктураЛога.Вставить("responseBody", ТелоОтвета);
        КонецЕсли;
    Исключение
        СтруктураЛога.Вставить("responseSize", 0);
    КонецПопытки;
    
    СтрокаЛога = СериализоватьВJSON(СтруктураЛога);
    
    УровеньЛога = УровеньЖурналаРегистрации.Информация;
    Если Ответ.КодСостояния >= 400 Тогда
        УровеньЛога = УровеньЖурналаРегистрации.Предупреждение;
    КонецЕсли;
    
    ЗаписьЖурналаРегистрации("HTTP.ИсходящийОтвет", УровеньЛога,,,СтрокаЛога);
    
КонецФункции

Мониторинг и метрики

Интеграция с системами мониторинга

// Процедура для отправки метрик в Prometheus Push Gateway
Процедура ОтправитьМетрикиAPI(ИмяМетрики, Значение, Теги = Неопределено) Экспорт
    
    Попытка
        СтруктураМетрики = Новый Структура;
        СтруктураМетрики.Вставить("metric", ИмяМетрики);
        СтруктураМетрики.Вставить("value", Значение);
        СтруктураМетрики.Вставить("timestamp", ТекущаяУниверсальнаяДатаВМиллисекундах());
        СтруктураМетрики.Вставить("instance", ПолучитьИмяКомпьютера());
        СтруктураМетрики.Вставить("job", "1c-api");
        
        Если Теги <> Неопределено Тогда
            СтруктураМетрики.Вставить("labels", Теги);
        КонецЕсли;
        
        // Формирование Prometheus формата
        СтрокаМетрики = СформироватьPrometheusМетрику(СтруктураМетрики);
        
        // Отправка в Push Gateway
        SSL = Новый ЗащищенноеСоединениеOpenSSL();
        Соединение = Новый HTTPСоединение("monitoring.company.ru", 9091,,,,30, SSL);
        
        Запрос = Новый HTTPЗапрос("/metrics/job/1c-api/instance/" + ПолучитьИмяКомпьютера());
        Запрос.УстановитьТелоИзСтроки(СтрокаМетрики, "UTF-8");
        Запрос.Заголовки.Вставить("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
        
        Ответ = Соединение.ОтправитьДляОбработки(Запрос);
        
        Если Ответ.КодСостояния <> 200 Тогда
            ЗаписьЖурналаРегистрации("МониторингAPI.Ошибка", 
                УровеньЖурналаРегистрации.Предупреждение,,,
                СтрШаблон("Push Gateway вернул код %1 для метрики %2", 
                    Ответ.КодСостояния, ИмяМетрики));
        КонецЕсли;
        
    Исключение
        // Ошибки мониторинга не должны влиять на основную логику
        ЗаписьЖурналаРегистрации("МониторингAPI.Исключение", 
            УровеньЖурналаРегистрации.Предупреждение,,,
            СтрШаблон("Не удалось отправить метрику %1: %2", 
                ИмяМетрики, ОписаниеОшибки()));
    КонецПопытки;
    
КонецПроцедуры

Функция СформироватьPrometheusМетрику(СтруктураМетрики)
    
    СтрокаМетрики = СтруктураМетрики.metric;
    
    // Добавление меток
    Если СтруктураМетрики.Свойство("labels") И СтруктураМетрики.labels.Количество() > 0 Тогда
        МассивМеток = Новый Массив;
        Для Каждого Метка Из СтруктураМетрики.labels Цикл
            МассивМеток.Добавить(СтрШаблон("%1=""%2""", Метка.Ключ, Метка.Значение));
        КонецЦикла;
        СтрокаМетрики = СтрокаМетрики + "{" + СтрСоединить(МассивМеток, ",") + "}";
    КонецЕсли;
    
    // Добавление значения и времени
    СтрокаМетрики = СтрокаМетрики + " " + СтруктураМетрики.value;
    
    Если СтруктураМетрики.Свойство("timestamp") Тогда
        СтрокаМетрики = СтрокаМетрики + " " + СтруктураМетрики.timestamp;
    КонецЕсли;
    
    Возврат СтрокаМетрики + Символы.ПС;
    
КонецФункции

Health Check и статус API

// HTTP-сервис для проверки состояния
Функция HealthGET(Запрос)
    
    Попытка
        НачалоПроверки = ТекущаяУниверсальнаяДата();
        
        СтатусПроверки = Новый Структура;
        СтатусПроверки.Вставить("status", "healthy");
        СтатусПроверки.Вставить("timestamp", XMLСтрока(НачалоПроверки));
        СтатусПроверки.Вставить("version", "1.0.0");
        СтатусПроверки.Вставить("checks", Новый Структура);
        
        // Проверка базы данных
        ПроверкаБД = ПроверитьСостояниеБазыДанных();
        СтатусПроверки.checks.Вставить("database", ПроверкаБД);
        
        // Проверка внешних зависимостей
        ПроверкаВнешнихAPI = ПроверитьВнешниеЗависимости();
        СтатусПроверки.checks.Вставить("external_apis", ПроверкаВнешнихAPI);
        
        // Проверка дискового пространства
        ПроверкаДиска = ПроверитьДисковоеПространство();
        СтатусПроверки.checks.Вставить("disk_space", ПроверкаДиска);
        
        // Проверка памяти
        ПроверкаПамяти = ПроверитьИспользованиеПамяти();
        СтатусПроверки.checks.Вставить("memory", ПроверкаПамяти);
        
        // Общий статус
        ОбщийСтатус = "healthy";
        Для Каждого Проверка Из СтатусПроверки.checks Цикл
            Если Проверка.Значение.status <> "healthy" Тогда
                ОбщийСтатус = "unhealthy";
                Прервать;
            КонецЕсли;
        КонецЦикла;
        
        СтатусПроверки.status = ОбщийСтатус;
        
        ВремяПроверки = (ТекущаяУниверсальнаяДата() - НачалоПроверки) * 1000;
        СтатусПроверки.Вставить("response_time_ms", ВремяПроверки);
        
        СтрокаJSON = СериализоватьВJSON(СтатусПроверки);
        
        КодОтвета = ?(ОбщийСтатус = "healthy", 200, 503);
        
        Ответ = Новый HTTPСервисОтвет(КодОтвета);
        Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
        Ответ.Заголовки.Вставить("Cache-Control", "no-cache");
        Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8", 
            ИспользованиеByteOrderMark.НеИспользовать);
            
        Возврат Ответ;
        
    Исключение
        СтатусОшибки = Новый Структура;
        СтатусОшибки.Вставить("status", "error");
        СтатусОшибки.Вставить("error", ОписаниеОшибки());
        СтатусОшибки.Вставить("timestamp", XMLСтрока(ТекущаяУниверсальнаяДата()));
        
        СтрокаJSON = СериализоватьВJSON(СтатусОшибки);
        
        Ответ = Новый HTTPСервисОтвет(500);
        Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
        Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8", 
            ИспользованиеByteOrderMark.НеИспользовать);
            
        Возврат Ответ;
    КонецПопытки;
    
КонецФункции

Функция ПроверитьСостояниеБазыДанных()
    
    Результат = Новый Структура;
    
    Попытка
        НачалоПроверки = ТекущаяУниверсальнаяДата();
        
        // Простой запрос для проверки доступности БД
        Запрос = Новый Запрос;
        Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 1 КАК Проверка";
        Запрос.Выполнить();
        
        ВремяОтклика = (ТекущаяУниверсальнаяДата() - НачалоПроверки) * 1000;
        
        Результат.Вставить("status", "healthy");
        Результат.Вставить("response_time_ms", ВремяОтклика);
        Результат.Вставить("message", "Database connection OK");
        
    Исключение
        Результат.Вставить("status", "unhealthy");
        Результат.Вставить("message", "Database connection failed");
        Результат.Вставить("error", ОписаниеОшибки());
    КонецПопытки;
    
    Возврат Результат;
    
КонецФункции

Оптимизация производительности

Пулинг соединений и кэширование

// Общий модуль УправлениеHTTPСоединениями

Перем ПулСоединений; // Глобальная переменная модуля

Функция ПолучитьСоединениеИзПула(Сервер, Порт, SSL = Ложь) Экспорт
    
    Если ПулСоединений = Неопределено Тогда
        ПулСоединений = Новый Соответствие;
    КонецЕсли;
    
    КлючСоединения = СтрШаблон("%1:%2:%3", Сервер, Порт, SSL);
    
    // Проверяем наличие соединения в пуле
    Если ПулСоединений.Получить(КлючСоединения) <> Неопределено Тогда
        ДанныеСоединения = ПулСоединений.Получить(КлючСоединения);
        
        // Проверяем, не истек ли таймаут соединения
        Если ТекущаяУниверсальнаяДата() - ДанныеСоединения.ВремяСоздания < 300 Тогда // 5 минут
            ДанныеСоединения.КоличествоИспользований = ДанныеСоединения.КоличествоИспользований + 1;
            Возврат ДанныеСоединения.Соединение;
        Иначе
            // Удаляем устаревшее соединение
            ПулСоединений.Удалить(КлючСоединения);
        КонецЕсли;
    КонецЕсли;
    
    // Создаем новое соединение
    Попытка
        Если SSL Тогда
            SSLСоединение = Новый ЗащищенноеСоединениеOpenSSL();
            НовоеСоединение = Новый HTTPСоединение(Сервер, Порт,,,,60, SSLСоединение);
        Иначе
            НовоеСоединение = Новый HTTPСоединение(Сервер, Порт,,,,60);
        КонецЕсли;
        
        // Сохраняем в пул
        ДанныеСоединения = Новый Структура;
        ДанныеСоединения.Вставить("Соединение", НовоеСоединение);
        ДанныеСоединения.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
        ДанныеСоединения.Вставить("КоличествоИспользований", 1);
        
        ПулСоединений.Вставить(КлючСоединения, ДанныеСоединения);
        
        Возврат НовоеСоединение;
        
    Исключение
        ВызватьИсключение СтрШаблон("Не удалось создать соединение с %1:%2: %3", 
            Сервер, Порт, ОписаниеОшибки());
    КонецПопытки;
    
КонецФункции

Процедура ОчиститьПулСоединений() Экспорт
    
    Если ПулСоединений = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    МассивДляУдаления = Новый Массив;
    ТекущееВремя = ТекущаяУниверсальнаяДата();
    
    Для Каждого ЭлементПула Из ПулСоединений Цикл
        ДанныеСоединения = ЭлементПула.Значение;
        
        // Удаляем соединения старше 10 минут или неиспользуемые
        Если (ТекущееВремя - ДанныеСоединения.ВремяСоздания > 600) ИЛИ
           (ДанныеСоединения.КоличествоИспользований = 0) Тогда
            МассивДляУдаления.Добавить(ЭлементПула.Ключ);
        КонецЕсли;
    КонецЦикла;
    
    Для Каждого Ключ Из МассивДляУдаления Цикл
        ПулСоединений.Удалить(Ключ);
    КонецЦикла;
    
КонецПроцедуры

Оптимизация JSON сериализации

// Кэширование часто используемых объектов JSON
Перем КэшJSON; // Глобальная переменная модуля

Функция СериализоватьВJSONСКэшем(Данные, КлючКэша = "", ВремяЖизни = 300) Экспорт
    
    Если КэшJSON = Неопределено Тогда
        КэшJSON = Новый Соответствие;
    КонецЕсли;
    
    // Если указан ключ кэша, проверяем наличие
    Если НЕ ПустаяСтрока(КлючКэша) Тогда
        КэшированныеДанные = КэшJSON.Получить(КлючКэша);
        Если КэшированныеДанные <> Неопределено Тогда
            Если ТекущаяУниверсальнаяДата() - КэшированныеДанные.ВремяСоздания < ВремяЖизни Тогда
                Возврат КэшированныеДанные.JSON;
            Иначе
                КэшJSON.Удалить(КлючКэша);
            КонецЕсли;
        КонецЕсли;
    КонецЕсли;
    
    // Сериализация с оптимизированными настройками
    ЗаписьJSON = Новый ЗаписьJSON;
    ЗаписьJSON.УстановитьСтроку();
    
    НастройкиСериализации = Новый НастройкиСериализацииJSON;
    НастройкиСериализации.ФорматДаты = ФорматДатыJSON.ISO;
    НастройкиСериализации.ВариантЗаписиДаты = ВариантЗаписиДатыJSON.Универсальная;
    НастройкиСериализации.ПереносСтрок = ПереносСтрокJSON.Нет; // Минимизируем размер
    НастройкиСериализации.ОтступыJSON = ОтступыJSON.Нет; // Убираем отступы
    НастройкиСериализации.ЭкранированиеСлешей = ЭкранированиеСлешейJSON.НеЭкранировать;
    
    ЗаписатьJSON(ЗаписьJSON, Данные, НастройкиСериализации);
    РезультатJSON = ЗаписьJSON.Закрыть();
    
    // Сохранение в кэш
    Если НЕ ПустаяСтрока(КлючКэша) Тогда
        КэшированныеДанные = Новый Структура;
        КэшированныеДанные.Вставить("JSON", РезультатJSON);
        КэшированныеДанные.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
        
        КэшJSON.Вставить(КлючКэша, КэшированныеДанные);
    КонецЕсли;
    
    Возврат РезультатJSON;
    
КонецФункции

Потоковая обработка больших данных

// Функция для обработки больших объемов данных порциями
Функция ОбработатьБольшойЗапросПорциями(Запрос, РазмерПорции = 1000) Экспорт
    
    // Получение общего количества записей
    ЗапросПодсчета = Новый Запрос;
    ЗапросПодсчета.Текст = СтрЗаменить(Запрос.Текст, "ВЫБРАТЬ", "ВЫБРАТЬ КОЛИЧЕСТВО(*) КАК Количество ИЗ (ВЫБРАТЬ");
    ЗапросПодсчета.Текст = ЗапросПодсчета.Текст + ") КАК ВложенныйЗапрос";
    
    // Копируем параметры
    Для Каждого Параметр Из Запрос.Параметры Цикл
        ЗапросПодсчета.УстановитьПараметр(Параметр.Ключ, Параметр.Значение);
    КонецЦикла;
    
    ОбщееКоличество = ЗапросПодсчета.Выполнить().Выгрузить()[0].Количество;
    
    МассивРезультатов = Новый Массив;
    Смещение = 0;
    
    Пока Смещение < ОбщееКоличество Цикл
        
        // Модифицируем запрос для получения порции
        ТекстЗапросаПорции = Запрос.Текст;
        
        // Добавляем ПЕРВЫЕ и смещение
        Если СтрНайти(ВРег(ТекстЗапросаПорции), "ПЕРВЫЕ") = 0 Тогда
            ТекстЗапросаПорции = СтрЗаменить(ТекстЗапросаПорции, "ВЫБРАТЬ", 
                СтрШаблон("ВЫБРАТЬ ПЕРВЫЕ %1 ПРОПУСТИТЬ %2", РазмерПорции, Смещение));
        КонецЕсли;
        
        ЗапросПорции = Новый Запрос;
        ЗапросПорции.Текст = ТекстЗапросаПорции;
        
        // Копируем параметры
        Для Каждого Параметр Из Запрос.Параметры Цикл
            ЗапросПорции.УстановитьПараметр(Параметр.Ключ, Параметр.Значение);
        КонецЦикла;
        
        РезультатПорции = ЗапросПорции.Выполнить();
        Выборка = РезультатПорции.Выбрать();
        
        ПорцияДанных = Новый Массив;
        Пока Выборка.Следующий() Цикл
            // Преобразование записи в структуру
            СтруктураЗаписи = Новый Структура;
            Для Колонка = 0 По РезультатПорции.Колонки.Количество() - 1 Цикл
                ИмяКолонки = РезультатПорции.Колонки[Колонка].Имя;
                ЗначениеКолонки = Выборка[ИмяКолонки];
                СтруктураЗаписи.Вставить(ИмяКолонки, ЗначениеКолонки);
            КонецЦикла;
            
            ПорцияДанных.Добавить(СтруктураЗаписи);
        КонецЦикла;
        
        МассивРезультатов.Добавить(ПорцияДанных);
        Смещение = Смещение + РазмерПорции;
        
        // Даем возможность другим процессам выполниться
        Если Смещение % (РазмерПорции * 10) = 0 Тогда
            Приостановить(50); // 50 мс
        КонецЕсли;
        
    КонецЦикла;
    
    Возврат МассивРезультатов;
    
КонецФункции

Типичные ошибки и их решение

Проблемы с кодировкой

Симптомы: Некорректное отображение русских символов в JSON-ответах, знаки вопросов вместо кириллицы.

Решение:

// НЕПРАВИЛЬНО - не указана кодировка
Ответ.УстановитьТелоИзСтроки(СтрокаJSON);

// ПРАВИЛЬНО - явно указываем UTF-8 без BOM
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8", 
    ИспользованиеByteOrderMark.НеИспользовать);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");

Ошибки SSL-сертификатов

Симптомы: "Удаленный узел не прошел проверку" при HTTPS-запросах к внешним API.

Решения:

// 1. В файле conf.cfg кластера серверов (только для разработки!)
IgnoreServerCertificatesChainRevocationSoftFail=true

// 2. Программное игнорирование для конкретного соединения (небезопасно!)
SSL = Новый ЗащищенноеСоединениеOpenSSL(,
    Новый СертификатыУдостоверяющихЦентровФайл(""));

// 3. ПРАВИЛЬНО - использование собственного хранилища сертификатов
СертификатыУЦ = Новый СертификатыУдостоверяющихЦентровWindows();
SSL = Новый ЗащищенноеСоединениеOpenSSL(, СертификатыУЦ);

// 4. Для проверки конкретного отпечатка сертификата
КлиентскиеСертификаты = Новый МассивКлиентскихСертификатов;
SSL = Новый ЗащищенноеСоединениеOpenSSL(КлиентскиеСертификаты, СертификатыУЦ);

CORS ошибки в веб-приложениях

Симптомы: "Cross-Origin Request Blocked" в консоли браузера, запросы с фронтенда блокируются.

Решение - полная настройка CORS:

// Обработка CORS preflight запросов
Функция КлиентыOPTIONS(Запрос)
    
    Ответ = Новый HTTPСервисОтвет(204); // No Content
    
    // Основные CORS заголовки
    Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*"); // или конкретный домен
    Ответ.Заголовки.Вставить("Access-Control-Allow-Methods", 
        "GET, POST, PUT, DELETE, OPTIONS, PATCH");
    Ответ.Заголовки.Вставить("Access-Control-Allow-Headers", 
        "Content-Type, Authorization, X-API-Key, X-Requested-With");
    Ответ.Заголовки.Вставить("Access-Control-Max-Age", "86400"); // 24 часа
    
    // Дополнительные заголовки для credentials
    Ответ.Заголовки.Вставить("Access-Control-Allow-Credentials", "true");
    Ответ.Заголовки.Вставить("Access-Control-Expose-Headers", 
        "X-Total-Count, X-Page-Count, Location");
    
    Возврат Ответ;
    
КонецФункции

// Добавление CORS заголовков во все ответы
Функция ДобавитьCORSЗаголовки(Ответ, Запрос)
    
    Origin = Запрос.Заголовки.Получить("Origin");
    
    // Проверка разрешенных доменов
    РазрешенныеДомены = Новый Массив;
    РазрешенныеДомены.Добавить("https://myapp.com");
    РазрешенныеДомены.Добавить("https://dev.myapp.com");
    РазрешенныеДомены.Добавить("http://localhost:3000"); // для разработки
    
    Если НЕ ПустаяСтрока(Origin) И (РазрешенныеДомены.Найти(Origin) <> Неопределено) Тогда
        Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", Origin);
    Иначе
        Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
    КонецЕсли;
    
    Ответ.Заголовки.Вставить("Access-Control-Allow-Credentials", "true");
    Ответ.Заголовки.Вставить("Vary", "Origin");
    
КонецФункции

Проблемы с таймаутами

Симптомы: Запросы прерываются по таймауту, особенно при работе с медленными внешними API.

Решения:

// 1. Настройка таймаутов на разных уровнях
Соединение = Новый HTTPСоединение("api.external.com", 443,,,,
    120, // Таймаут соединения в секундах
    SSL);

// 2. Таймаут конкретного запроса (приоритетнее)
Запрос = Новый HTTPЗапрос("/api/slow-endpoint");
Запрос.УстановитьТаймаут(180); // 3 минуты для медленного API

// 3. Адаптивные таймауты в зависимости от операции
Функция ПолучитьТаймаутДляОперации(ТипОперации)
    
    Таймауты = Новый Структура;
    Таймауты.Вставить("Авторизация", 30);
    Таймауты.Вставить("Получение", 60);
    Таймауты.Вставить("Создание", 120);
    Таймауты.Вставить("Обновление", 90);
    Таймауты.Вставить("Удаление", 60);
    Таймауты.Вставить("Отчеты", 300); // 5 минут для отчетов
    
    Возврат Таймауты.Получить(ТипОперации, 60); // По умолчанию 60 сек
    
КонецФункции

Проблемы с размером данных

Симптомы: Ошибки при передаче больших JSON объектов, превышение лимитов HTTP-сервера.

Решения:

// 1. Сжатие данных
Функция СжатьJSON(СтрокаJSON)
    
    Попытка
        ДвоичныеДанные = ПолучитьДвоичныеДанныеИзСтроки(СтрокаJSON, "UTF-8");
        СжатыеДанные = СжатьДанные(ДвоичныеДанные);
        Возврат СжатыеДанные;
    Исключение
        ВызватьИсключение "Ошибка сжатия данных: " + ОписаниеОшибки();
    КонецПопытки;
    
КонецФункции

// 2. Пагинация больших результатов
Функция ПолучитьДанныеСПагинацией(ОбщийЗапрос, НомерСтраницы, РазмерСтраницы)
    
    МаксимальныйРазмерСтраницы = 1000; // Ограничение безопасности
    РазмерСтраницы = Мин(РазмерСтраницы, МаксимальныйРазмерСтраницы);
    
    Смещение = (НомерСтраницы - 1) * РазмерСтраницы;
    
    // Модификация запроса для пагинации
    ТекстЗапроса = СтрЗаменить(ОбщийЗапрос.Текст, "ВЫБРАТЬ", 
        СтрШаблон("ВЫБРАТЬ ПЕРВЫЕ %1 ПРОПУСТИТЬ %2", РазмерСтраницы, Смещение));
    
    ЗапросСтраницы = Новый Запрос;
    ЗапросСтраницы.Текст = ТекстЗапроса;
    
    // Копирование параметров
    Для Каждого Параметр Из ОбщийЗапрос.Параметры Цикл
        ЗапросСтраницы.УстановитьПараметр(Параметр.Ключ, Параметр.Значение);
    КонецЦикла;
    
    Возврат ЗапросСтраницы.Выполнить();
    
КонецФункции

Версионирование API

Стратегии версионирования

В 1С рекомендуется использовать версионирование через URL path для максимальной совместимости:

// Структура URL с версионированием
// /hs/api/v1/clients - версия 1
// /hs/api/v2/clients - версия 2  
// /hs/api/v3/clients - версия 3

Функция ОпределитьВерсиюAPI(Запрос) Экспорт
    
    ОтносительныйURL = Запрос.ОтносительныйURL;
    
    // Извлечение версии из URL
    Если СтрНайти(ОтносительныйURL, "/v3/") > 0 Тогда
        Возврат "v3";
    ИначеЕсли СтрНайти(ОтносительныйURL, "/v2/") > 0 Тогда
        Возврат "v2";
    ИначеЕсли СтрНайти(ОтносительныйURL, "/v1/") > 0 Тогда
        Возврат "v1";
    Иначе
        // Версия по умолчанию для обратной совместимости
        Возврат "v1";
    КонецЕсли;
    
КонецФункции

// Маршрутизация по версиям
Функция КлиентыGET(Запрос)
    
    ВерсияAPI = ОпределитьВерсиюAPI(Запрос);
    
    Если ВерсияAPI = "v1" Тогда
        Возврат КлиентыGET_V1(Запрос);
    ИначеЕсли ВерсияAPI = "v2" Тогда
        Возврат КлиентыGET_V2(Запрос);
    ИначеЕсли ВерсияAPI = "v3" Тогда
        Возврат КлиентыGET_V3(Запрос);
    Иначе
        Возврат СформироватьОтветОбОшибке(400, "Неподдерживаемая версия API");
    КонецЕсли;
    
КонецФункции

Управление жизненным циклом версий

// Информация о версиях API
Функция ПолучитьИнформациюОВерсияхAPI() Экспорт
    
    ВерсииAPI = Новый Структура;
    
    // Версия 1.0 - устаревшая (deprecated)
    Версия1 = Новый Структура;
    Версия1.Вставить("version", "1.0");
    Версия1.Вставить("status", "deprecated");
    Версия1.Вставить("deprecatedDate", "2024-01-01");
    Версия1.Вставить("sunsetDate", "2025-12-31");
    Версия1.Вставить("description", "Первая версия API, будет отключена 31.12.2025");
    
    // Версия 2.0 - текущая стабильная
    Версия2 = Новый Структура;
    Версия2.Вставить("version", "2.0");
    Версия2.Вставить("status", "stable");
    Версия2.Вставить("releaseDate", "2024-06-01");
    Версия2.Вставить("description", "Текущая стабильная версия с расширенными возможностями");
    
    // Версия 3.0 - в разработке (beta)
    Версия3 = Новый Структура;
    Версия3.Вставить("version", "3.0");
    Версия3.Вставить("status", "beta");
    Версия3.Вставить("releaseDate", "2025-03-01");
    Версия3.Вставить("description", "Новая версия с поддержкой GraphQL и улучшенной безопасностью");
    
    ВерсииAPI.Вставить("v1", Версия1);
    ВерсииAPI.Вставить("v2", Версия2);
    ВерсииAPI.Вставить("v3", Версия3);
    
    Возврат ВерсииAPI;
    
КонецФункции

// Предупреждения об устаревших версиях
Функция ДобавитьПредупрежденияОВерсии(Ответ, ВерсияAPI)
    
    ИнформацияОВерсиях = ПолучитьИнформациюОВерсияхAPI();
    ТекущаяВерсия = ИнформацияОВерсиях.Получить(ВерсияAPI);
    
    Если ТекущаяВерсия <> Неопределено Тогда
        
        Если ТекущаяВерсия.status = "deprecated" Тогда
            Ответ.Заголовки.Вставить("Warning", 
                СтрШаблон("299 - \"API version %1 is deprecated. Sunset date: %2\"", 
                    ТекущаяВерсия.version, ТекущаяВерсия.sunsetDate));
            Ответ.Заголовки.Вставить("Sunset", ТекущаяВерсия.sunsetDate);
            
        ИначеЕсли ТекущаяВерсия.status = "beta" Тогда
            Ответ.Заголовки.Вставить("Warning", 
                СтрШаблон("299 - \"API version %1 is in beta. Subject to change.\"", 
                    ТекущаяВерсия.version));
        КонецЕсли;
        
        Ответ.Заголовки.Вставить("API-Version", ТекущаяВерсия.version);
        
    КонецЕсли;
    
КонецФункции

Миграция между версиями

// Адаптер для совместимости между версиями
Функция АдаптироватьДанныеМеждуВерсиями(Данные, ИсходнаяВерсия, ЦелеваяВерсия) Экспорт
    
    Если ИсходнаяВерсия = ЦелеваяВерсия Тогда
        Возврат Данные; // Версии одинаковые
    КонецЕсли;
    
    // Миграция с v1 на v2
    Если ИсходнаяВерсия = "v1" И ЦелеваяВерсия = "v2" Тогда
        Возврат МигрироватьV1наV2(Данные);
    КонецЕсли;
    
    // Миграция с v2 на v3
    Если ИсходнаяВерсия = "v2" И ЦелеваяВерсия = "v3" Тогда
        Возврат МигрироватьV2наV3(Данные);
    КонецЕсли;
    
    // Обратная миграция с v2 на v1
    Если ИсходнаяВерсия = "v2" И ЦелеваяВерсия = "v1" Тогда
        Возврат МигрироватьV2наV1(Данные);
    КонецЕсли;
    
    ВызватьИсключение СтрШаблон("Миграция с %1 на %2 не поддерживается", 
        ИсходнаяВерсия, ЦелеваяВерсия);
    
КонецФункции

Функция МигрироватьV1наV2(ДанныеV1)
    
    ДанныеV2 = Новый Структура;
    
    // Копируем совместимые поля
    Если ДанныеV1.Свойство("id") Тогда
        ДанныеV2.Вставить("id", ДанныеV1.id);
    КонецЕсли;
    
    Если ДанныеV1.Свойство("name") Тогда
        ДанныеV2.Вставить("fullName", ДанныеV1.name); // Переименование поля
    КонецЕсли;
    
    // Новые поля в v2 с значениями по умолчанию
    ДанныеV2.Вставить("version", "2.0");
    ДанныеV2.Вставить("metadata", Новый Структура);
    ДанныеV2.metadata.Вставить("migratedFrom", "v1");
    ДанныеV2.metadata.Вставить("migrationDate", XMLСтрока(ТекущаяУниверсальнаяДата()));
    
    // Структурирование данных
    Если ДанныеV1.Свойство("phone") ИЛИ ДанныеV1.Свойство("email") Тогда
        Контакты = Новый Структура;
        
        Если ДанныеV1.Свойство("phone") Тогда
            Контакты.Вставить("phone", ДанныеV1.phone);
        КонецЕсли;
        
        Если ДанныеV1.Свойство("email") Тогда
            Контакты.Вставить("email", ДанныеV1.email);
        КонецЕсли;
        
        ДанныеV2.Вставить("contacts", Контакты);
    КонецЕсли;
    
    Возврат ДанныеV2;
    
КонецФункции

Заключение и лучшие практики

REST API в 1С:Предприятие — мощный инструмент для современных интеграций. Платформа предоставляет как автоматические решения через OData, так и гибкие возможности создания собственных HTTP-сервисов.

Ключевые принципы успешной реализации:

🛡️ Безопасность

  • Всегда используйте HTTPS в production
  • Реализуйте современную аутентификацию (JWT предпочтительнее Basic Auth)
  • Применяйте принцип минимальных привилегий
  • Логируйте все попытки доступа и подозрительную активность
  • Используйте rate limiting для предотвращения злоупотреблений

⚡ Производительность

  • Используйте пулы HTTP-соединений для клиентских запросов
  • Настройте пулы сессий HTTP-сервисов (sessionLifetime, sessionMaxCount)
  • Реализуйте кэширование на разных уровнях (данные, JSON, HTTP заголовки)
  • Применяйте пагинацию для больших наборов данных
  • Оптимизируйте запросы к базе данных
  • Используйте сжатие для больших ответов

🔍 Мониторинг и диагностика

  • Логируйте все операции с соответствующими уровнями
  • Отслеживайте ключевые метрики (время ответа, коды ошибок, throughput)
  • Реализуйте health checks для проверки состояния системы
  • Используйте correlation ID для трассировки запросов
  • Настройте алерты на критические события

🔄 Надежность

  • Реализуйте retry механизмы с экспоненциальной задержкой
  • Обрабатывайте все типы ошибок (4xx, 5xx, network)
  • Используйте circuit breaker для внешних зависимостей
  • Настройте адекватные таймауты
  • Реализуйте graceful degradation

Чек-лист для production deployment

🚀 Готовность к продакшену:

  • ✅ Все endpoints покрыты тестами
  • ✅ Настроена аутентификация и авторизация
  • ✅ Реализованы rate limiting и throttling
  • ✅ Настроено логирование и мониторинг
  • ✅ Проведен security audit
  • ✅ Настроены health checks
  • ✅ Документация API актуальна
  • ✅ Настроены алерты и dashboards
  • ✅ Проведено нагрузочное тестирование
  • ✅ Настроен backup и disaster recovery план

Дальнейшие шаги развития

После освоения базовых принципов REST API в 1С рекомендуется изучить:

  • GraphQL интеграции — для более гибкого получения данных
  • WebSocket поддержку — для real-time уведомлений
  • API Gateway паттерны — для масштабируемых микросервисных архитектур
  • Event-driven архитектуру — для асинхронной обработки
  • Distributed tracing — для отладки сложных интеграций
  • API-first подход — для проектирования новых систем

Полезные ресурсы

Заключение: REST API в 1С открывает широкие возможности для построения современной IT-архитектуры предприятия. Правильная реализация с учетом принципов безопасности, производительности и надежности позволит создать масштабируемую интеграционную платформу, способную обеспечить бесшовное взаимодействие с внешними системами и поддержать цифровую трансформацию бизнеса.

Дополнительные аспекты

Производственный контур требует внимания к производительности REST API 1С: профилируйте время ответа, оптимизируйте структуру данных, уменьшайте глубину вложенности JSON. Для обработки ошибок REST 1С важно стандартизировать формат ответа и включать correlation-id для трассировки.

При большом количестве вызовов используйте пулы HTTP-сервисов 1С и выставляйте корректные таймауты соединений. Снижайте накладные расходы сериализации за счёт переиспользования буферов. Включайте кэш ETag/If-None-Match и сжатие через reverse-прокси.

Статья опубликована в сентябре 2025 года. Актуально для 1С:Предприятие 8.3.25+.