Интеграция систем 1С с внешними веб-сервисами по протоколу SOAP с использованием статических или динамических WSDL-определений — стандартная задача для современного разработчика. Однако в процессе реализации обмена можно столкнуться с трудночитаемыми системными ошибками. Одной из самых распространенных и коварных является ошибка: «При вызове веб-сервиса произошла ошибка. Неизвестная ошибка. Ошибка разбора XML: - [1,1] Фатальная ошибка: Extra content at the end of the document».
В этой статье мы подробно разберем, почему возникает эта ошибка, как специфика импорта WSDL-схем в 1С влияет на передачу параметров, как правильно использовать инструменты отладки для локализации проблемы, и как написать надежный и отказоустойчивый код для работы с SOAP-сервисами.
Для начала проанализируем, что именно пытается сказать нам платформа 1С. Ошибка разбора XML с указанием позиции [1,1] (первая строка, первый символ) и текстом Extra content at the end of the document (лишнее содержимое в конце документа) означает, что встроенный парсер XML ожидал получить валидный SOAP-конверт, оформленный по всем правилам XML-стандарта, но вместо этого получил некорректные данные.
Почему так происходит? Когда 1С отправляет на сторонний веб-сервис некорректно сформированный SOAP-запрос, удаленный веб-сервер (например, IIS, Kestrel или Apache) не может его обработать. Вместо генерации стандартного XML-ответа с описанием ошибки (SOAP Fault), сервер возвращает стандартную HTTP-страницу с кодом состояния 400 Bad Request или 500 Internal Server Error. Эта страница представляет собой обычный HTML-текст.
Платформа 1С, получив этот ответ, пытается распарсить его как XML. Поскольку HTML-код не соответствует строгому синтаксису XML (например, содержит незакрытые теги, специфичные сущности или лишний текст вне корневого элемента), парсер мгновенно спотыкается на первом же символе и выдает фатальное исключение. Таким образом, истинная причина ошибки разбора XML кроется не в самом ответе, а в том, что мы отправили веб-сервису неверный запрос, который «сломал» логику обработки на стороне сервера.
Рассмотрим подробнее, почему разработчики часто совершают ошибку при формировании параметров SOAP-запроса в 1С. В веб-сервисах, особенно разработанных на платформе .NET (WCF, ASMX), крайне популярен стиль проектирования WSDL, называемый Document-Literal Wrapped.
В этом стиле XML-схема описывает операцию как единый комплексный тип (обертку — wrapper), который содержит внутри себя фактические входные параметры. При импорте WSDL в 1С (через объект WSОпределения или механизм WS-ссылок) платформа выполняет автоматическое «разворачивание» (unwrapping) схемы:
Если же разработчик пытается создать объект операции через Прокси.ФабрикаXDTO.Создать() и передать его целиком, структура SOAP-конверта нарушается. Вместо ожидаемых простых тегов XML-парсер сервиса получает вложенный некорректный объект, что приводит к краху обработки на стороне сервера.
Посмотрим на пример кода, который изначально приводит к ошибке:
// Создаем определение веб-сервиса
WSОпределение = Новый WSОпределения(НашВебСервис);
WSСервис = WSОпределение.Сервисы[0];
Прокси = Новый WSПрокси(WSОпределение, WSСервис.URIПространстваИмен, WSСервис.Имя, WSСервис.ТочкиПодключения[0].Имя);
// ПОПЫТКА СОЗДАТЬ ОБЕРТКУ ВРУЧНУЮ (ОШИБКА!)
ТипWSПараметра = Прокси.ФабрикаXDTO.Пакеты.Получить((WSСервис.URIПространстваИмен)).Получить("ServisesWithoutSupplier");
WSПараметр = Прокси.ФабрикаXDTO.Создать(ТипWSПараметра);
WSПараметр.partnerID = НашКодПартнера;
serviceList = "";
Error = "";
// Вызов метода с передачей сложного объекта XDTO вместо плоских параметров
Прокси.ServisesWithoutSupplier(WSПараметр, serviceList, Error);
В данном случае метод ServisesWithoutSupplier ожидает на вход три параметра: строковое значение partnerID и две выходные переменные (serviceList и Error). Передача объекта WSПараметр целиком ломает сигнатуру вызова, генерируя неверный SOAP-запрос.
Разберем по шагам, как написать код правильно. Нам не нужно обращаться к фабрике XDTO для создания входного параметра операции. Мы передаем строковое значение кода партнера напрямую в качестве первого аргумента метода:
// Инициализируем WS-определение и прокси
WSОпределение = Новый WSОпределения(НашВебСервис);
WSСервис = WSОпределение.Сервисы[0];
Прокси = Новый WSПрокси(WSОпределение, WSСервис.URIПространстваИмен, WSСервис.Имя, WSСервис.ТочкиПодключения[0].Имя);
// Подготавливаем переменные для выходных параметров
ВыходнойСписокУслуг = Неопределено;
ВыходнаяОшибка = Неопределено;
// Передаем параметры правильно: плоские входные значения и переменные для результатов
Прокси.ServisesWithoutSupplier(НашКодПартнера, ВыходнойСписокУслуг, ВыходнаяОшибка);
Этот лаконичный вариант работает корректно, поскольку платформа 1С самостоятельно упаковывает переданную строку НашКодПартнера в тег <partnerID> внутри SOAP-тела <ServisesWithoutSupplier>, в полном соответствии с требованиями WSDL-схемы.
Если вы столкнулись со сложным веб-сервисом и стандартный вызов не работает, незаменимыми инструментами становятся SoapUI и Fiddler. Проанализируем алгоритм работы с ними:
ServisesWithoutSupplier. Замените шаблонные значения реальными данными и отправьте запрос. Убедитесь, что сервер возвращает корректный XML-ответ, содержащий элементы <ServiceInfo>. Если SoapUI возвращает ошибку, проблема на стороне самого веб-сервиса или в некорректности переданных данных.WSПрокси укажите использование прокси-сервера Fiddler:
ПроксиСервер = Новый ИнтернетПрокси;
ПроксиСервер.Установить("http", "127.0.0.1", 8888);
// Для WSПрокси задаем прокси-сервер при динамическом вызове через свойства системы
После успешного вызова метода вы получите ответ в переменной ВыходнойСписокУслуг. Часто разработчики предлагают использовать стандартный обход коллекции следующим образом:
Для Каждого Стр ИЗ ВыходнойСписокУслуг.ServiceInfo Цикл
// Обработка данных
КонецЦикла;
Внимание! Этот код таит в себе скрытую угрозу, которая обязательно проявит себя в рабочей базе. Дело в особенностях сериализации XDTO-пакетов в платформе 1С:
ServiceInfo является объектом типа СписокXDTO. Цикл Для Каждого отработает идеально.ОбъектXDTO. Попытка применить цикл Для Каждого к одиночному объекту вызовет критическую ошибку «Итератор для значения не определен».Неопределено, что также приведет к аварийному завершению программы.Для предотвращения подобных ситуаций необходимо использовать защищенный метод ПолучитьСписок(). Разберем безопасный вариант обхода коллекции:
Если ВыходнойСписокУслуг <> Неопределено Тогда
// Получаем гарантированную коллекцию элементов XDTO, независимо от их количества в ответе
СписокЭлементов = ВыходнойСписокУслуг.ПолучитьСписок("ServiceInfo");
Для Каждого ТекущаяУслуга Из СписокЭлементов Цикл
ИдентификаторУслуги = ТекущаяУслуга.SerID; // Тип: Число
НаименованиеУслуги = ТекущаяУслуга.SerName; // Тип: Строка
// Выполняем полезную работу
Сообщить("Получена услуга: " + НаименованиеУслуги + " (ID: " + ИдентификаторУслуги + ")");
КонецЦикла;
КонецЕсли;
Применение метода ПолучитьСписок("ИмяСвойства") гарантирует, что платформа всегда вернет итерируемый объект типа СписокXDTO (даже если там всего один элемент или список пуст), что делает ваше интеграционное решение полностью надежным и стабильным.