Как получить текст входящего запроса WS-соединения, если он некорректен и не доходит до кода 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Когда мы разрабатываем или поддерживаем веб-сервисы на платформе 1С:Предприятие, мы можем столкнуться с ситуацией, когда входящий запрос содержит некорректные данные. Например, если веб-сервис ожидает числовой параметр, а клиент передает текстовую строку. В таких случаях на клиенте может появиться ошибка типа "Несоответствие типов (параметр номер '1')", а в журнале регистрации 1С мы увидим лишь записи о начале и завершении сеанса, но не сможем найти информацию о самом некорректном запросе или ошибке выполнения нашей процедуры веб-сервиса. Более того, даже если мы попробуем записать что-либо в журнал регистрации в самой первой строке процедуры веб-сервиса, эта запись не появится, что указывает на то, что до кода нашей процедуры дело просто не доходит.

Почему некорректные запросы не логируются в 1С?

Давайте выясним причину такого поведения. Проблема заключается в том, что валидация входящих данных для веб-сервиса происходит на более низком уровне, чем выполнение кода нашей процедуры в 1С. Взаимодействие с веб-сервисами 1С основывается на стандарте XDTO (XML Data Type Objects). Описание веб-сервиса содержит информацию о типах данных, которые ожидаются для каждого параметра. Когда веб-сервер (Apache или IIS) получает запрос к веб-сервису 1С, модуль расширения 1С для веб-сервера пытается десериализовать входящий XML-запрос и привести его к типам, описанным в XDTO-пакете нашего веб-сервиса. Если на этом этапе происходит несоответствие типов (например, в теге, который должен содержать число, приходит текст), возникает ошибка валидации XDTO. Эта ошибка происходит до того, как запрос будет передан для обработки в ядро 1С:Предприятия. Таким образом, код нашей процедуры веб-сервиса в 1С просто не запускается, и, соответственно, у нас нет возможности средствами 1С перехватить и залогировать сам некорректный запрос или ошибку валидации.

Именно поэтому стандартный журнал регистрации 1С остается пустым в отношении деталей ошибки. Ответ с ошибкой "HTTP/1.1 500 Internal server error" и сообщением о несоответствии типов данных XDTO формируется на уровне веб-сервера и отправляется клиенту. Мы видим, что ошибка генерируется еще до того, как запрос попадает в среду выполнения 1С. Поэтому, чтобы получить текст некорректного запроса, нам необходимо использовать механизмы, работающие на уровне веб-сервера или выше.

Способы получения текста некорректного WS-запроса

Перед нами стоит задача – перехватить и зафиксировать текст входящего запроса, даже если он содержит ошибки валидации и не доходит до нашего кода в 1С. Рассмотрим несколько подходов, которые помогут нам в этом:

  1. Логирование на уровне веб-сервера (Apache / IIS / NGINX).
  2. Использование HTTP-сервиса 1С в качестве прокси.

Каждый из этих методов имеет свои особенности, преимущества и недостатки, которые мы подробно разберем.

1. Логирование на уровне веб-сервера (Apache / IIS / NGINX)

Поскольку ошибка валидации XDTO возникает на уровне веб-сервера, логично предположить, что именно веб-сервер может предоставить нам необходимую информацию. Мы можем настроить веб-сервер таким образом, чтобы он логировал полное тело входящего HTTP-запроса, включая его содержимое. Это позволит нам увидеть, какой именно запрос привел к ошибке, даже если он не был обработан 1С.

1.1. Для Apache

Для Apache HTTP Server существуют несколько способов логирования тела запроса:

  1. Использование модуля mod_dumpio:
    • Этот модуль позволяет дампить (выводить) все входящие и исходящие данные на уровне ввода/вывода в файл error.log.
    • Для его использования сначала необходимо убедиться, что mod_dumpio скомпилирован и загружен (например, в файле httpd.conf должна быть строка LoadModule dumpio_module modules/mod_dumpio.so).
    • Затем нам нужно установить соответствующий уровень логирования в файле конфигурации Apache (обычно httpd.conf или в файле виртуального хоста): LogLevel dumpio:trace7 Уровень trace7 является самым подробным. После этих изменений Apache будет записывать все данные, проходящие через него, в error.log. Это может быть очень объемный лог, поэтому мы рекомендуем использовать его только для отладки.
    • В Apache версии 2.4.18 и выше, при включенном mod_dumpio, мы можем также использовать переменную %{requestbody} в директиве LogFormat для записи тела запроса непосредственно в лог доступа (обычно access.log). Пример:
      
      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %{requestbody}n" combined_body
      CustomLog "logs/access.log" combined_body
      
      Использование %{requestbody}n позволяет нам добавить тело запроса в пользовательский формат лога.
  2. Использование модуля mod_security:
    • mod_security – это межсетевой экран веб-приложений (WAF) для Apache. Помимо своих основных функций по безопасности, он может быть настроен на логирование полезной нагрузки POST-запросов.
    • Для этого нам потребуется настроить соответствующие правила в конфигурации mod_security, например, с помощью директивы SecAuditLogParts, которая определяет, какие части транзакции будут записываться в журнал аудита.
    • Это более сложное решение, но оно предоставляет широкие возможности по фильтрации и анализу запросов.

1.2. Для IIS

Для веб-сервера IIS стандартные логи обычно не включают данные POST-запросов (тело запроса), фиксируя только заголовки и строку запроса. Однако есть несколько способов получить эту информацию:

  1. Разработка собственного HTTP-модуля (IHttpModule на C#):
    • Мы можем разработать пользовательский HTTP-модуль на языке C# (реализовав интерфейс IHttpModule из пространства имен System.Web). Этот модуль будет перехватывать входящие HTTP-запросы на различных этапах конвейера обработки IIS.
    • В таком модуле мы можем получить доступ к потоку ввода запроса (HttpRequest.InputStream) и прочитать его содержимое до того, как оно будет обработано другими модулями или средой выполнения 1С.
    • Затем мы можем записать это содержимое в свой собственный лог-файл. Это позволяет реализовать гибкое логирование без изменения кода веб-приложения 1С и настраивается через файл web.config.

      Концептуальный пример кода C# для IHttpModule:

      
      using System;
      using System.IO;
      using System.Web;
      
      public class RequestBodyLoggerModule : IHttpModule
      {
          public void Init(HttpApplication context)
          {
              context.BeginRequest += new EventHandler(OnBeginRequest);
          }
      
          private void OnBeginRequest(object sender, EventArgs e)
          {
              HttpApplication application = (HttpApplication)sender;
              HttpRequest request = application.Context.Request;
      
              // Логировать только POST запросы и только для определенного пути, если нужно
              if (request.HttpMethod == "POST" /* && request.Path.Contains("/ws/MyWebService") */)
              {
                  try
                  {
                      request.InputStream.Position = 0; // Перемещаем указатель в начало потока
                      using (StreamReader reader = new StreamReader(request.InputStream, request.ContentEncoding))
                      {
                          string requestBody = reader.ReadToEnd();
                          // Здесь мы можем записать requestBody в наш лог-файл.
                          // Например, File.AppendAllText("C:\\Logs\\RequestBodyLog.txt", 
                          //                             DateTime.Now.ToString() + " " + request.Url.AbsoluteUri + 
                          //                             Environment.NewLine + requestBody + Environment.NewLine + "---" + Environment.NewLine);
                          
                          // Важно: после прочтения потока, если он не кэшируется IIS,
                          // его нужно сбросить в начало, чтобы другие обработчики могли его прочитать.
                          request.InputStream.Position = 0; 
                      }
                  }
                  catch (Exception ex)
                  {
                      // Логирование ошибок чтения потока
                  }
              }
          }
      
          public void Dispose() { }
      }
      

      Затем этот модуль необходимо зарегистрировать в файле web.config вашего веб-приложения 1С:

      
      <configuration>
        <system.webServer>
          <modules>
            <add name="RequestBodyLoggerModule" type="RequestBodyLoggerModule" />
          </modules>
        </system.webServer>
      </configuration>
      
  2. Failed Request Tracing (Трассировка неудачных запросов):
    • В IIS 7 и выше есть мощная функция Failed Request Tracing (FRT). Мы можем настроить ее для записи подробных данных о запросах, которые возвращают определенные коды состояния (например, 500 Internal Server Error), или для запросов к конкретному URL.
    • FRT может записывать очень детальную информацию о каждом этапе обработки запроса, включая содержимое POST-данных, что очень полезно для отладки.
    • Для настройки FRT нам нужно активировать эту функцию для сайта в IIS Manager и затем создать правило трассировки для наших веб-сервисов, указав, например, статус-код 500 и включив трассировку для ASP.NET и WWW Server событий.
  3. Расширенное логирование (Advanced Logging) в IIS 8.5+:
    • Advanced Logging – это отдельный модуль для IIS 8.5 и более поздних версий, который предоставляет более гибкие возможности по настройке логирования, чем стандартные логи.
    • Хотя он напрямую не логирует тело запроса в стандартных полях, он дает основу для добавления пользовательских полей и может быть расширен для выполнения этой задачи. Например, через интеграцию с PowerShell или другими скриптами.

1.3. Для NGINX

NGINX часто используется как обратный прокси перед веб-сервером (Apache/IIS), на котором опубликован веб-сервис 1С. NGINX предлагает гибкие возможности для логирования, включая тело запроса:

  1. Логирование тела запроса через переменную $request_body:
    • Мы можем включить переменную $request_body в формат лога NGINX (директива log_format) для записи тела запроса.
    • Чтобы NGINX мог получить доступ к телу запроса для логирования, может потребоваться предварительное чтение тела запроса с помощью директивы proxy_request_buffering off; (если NGINX выступает как прокси) или с использованием модулей, кэширующих тело запроса.
    • Пример настройки в nginx.conf:
      
      http {
          log_format custom_body '$remote_addr - $remote_user [$time_local] '
                                 '"$request" $status $body_bytes_sent '
                                 '"$http_referer" "$http_user_agent" '
                                 'request_body: "$request_body"';
      
          server {
              listen 80;
              server_name your_domain.com;
      
              access_log /var/log/nginx/access_body.log custom_body;
      
              location / {
                  proxy_pass http://localhost:8080; # Адрес вашего Apache/IIS с 1С
                  proxy_set_header Host $host;
                  proxy_set_header X-Real-IP $remote_addr;
                  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      
                  # Для логирования тела запроса NGINX должен его буферизовать
                  # или не буферизовать, в зависимости от сценария.
                  # Для $request_body обычно требуется, чтобы NGINX прочитал тело.
                  # Если тело запроса очень большое, это может повлиять на производительность.
                  # client_body_buffer_size 128k; # Размер буфера для тела запроса
                  # client_max_body_size 10m;    # Максимальный размер тела запроса
              }
          }
      }
      

      Важно помнить, что NGINX по умолчанию не читает полностью тело запроса, если оно не требуется для проксирования или других операций. Для того чтобы переменная $request_body была заполнена, NGINX должен быть настроен на чтение тела запроса.

  2. NGINX с поддержкой Lua (OpenResty):
    • При использовании NGINX с поддержкой Lua (например, OpenResty) мы получаем огромные возможности для написания скриптов, которые могут перехватывать запросы на различных этапах обработки.
    • С помощью Lua мы можем динамически читать тело запроса (функция ngx.req.read_body() и ngx.req.get_body_data()), анализировать его и записывать в лог только в определенных условиях, например, только для ошибочных запросов или запросов к конкретным URI.
    • Это позволяет нам реализовать выборочное логирование, что значительно снижает объем генерируемых логов.
    • Пример Lua-кода для NGINX (в контексте http или server):
      
      http {
          # ... другие настройки ...
      
          server {
              listen 80;
              server_name your_domain.com;
      
              access_log /var/log/nginx/access_custom.log combined; # Стандартный лог
      
              location /ws/ {
                  # Обязательно для чтения тела запроса в Lua
                  lua_need_request_body on; 
                  lua_code_cache off; # Отключить кэш для отладки
      
                  access_by_lua_block {
                      ngx.req.read_body() -- Читаем тело запроса
                      local body_data = ngx.req.get_body_data()
      
                      if body_data then
                          -- Здесь мы можем проверить содержимое body_data
                          -- и при необходимости записать его в отдельный лог
                          -- Например, если body_data содержит определенные ошибки,
                          -- или просто для всех запросов к WS
                          ngx.log(ngx.INFO, "Incoming WS Request Body: ", body_data)
                      end
                  }
      
                  proxy_pass http://localhost:8080; # Адрес вашего Apache/IIS с 1С
                  proxy_set_header Host $host;
                  proxy_set_header X-Real-IP $remote_addr;
                  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
              }
          }
      }
      

      С помощью Lua мы можем реализовать сложную логику, например, логировать тело запроса только в случае, если вышестоящий proxy_pass вернул ошибку (например, статус 500).

  3. Кэширование и снижение нагрузки:
    • Если к нам летят сотни запросов в секунду с ошибками, NGINX с его возможностями кэширования и фильтрации может значительно снизить нагрузку на 1С. Мы можем кэшировать ответы для корректных запросов, а также отфильтровывать или логировать только ошибочные.

Важно понимать, что постоянное логирование полного тела каждого запроса может значительно увеличить объем логов и повлиять на производительность сервера. Поэтому мы настоятельно рекомендуем настраивать такое логирование выборочно: либо только для отладки, либо только для запросов, которые приводят к ошибкам, либо для определенных URL.

2. Обходной путь средствами 1С: HTTP-сервис как прокси

Если по каким-либо причинам мы не можем или не хотим настраивать логирование на уровне веб-сервера, существует обходной путь, который позволяет нам получить текст запроса непосредственно средствами 1С. Этот метод заключается в создании собственного HTTP-сервиса в 1С, который будет выступать в роли прокси для нашего основного веб-сервиса.

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

  1. Создание HTTP-сервиса в 1С:
    • В конфигурации 1С мы создаем новый HTTP-сервис.
    • Для этого HTTP-сервиса мы определяем URI-шаблон, который будет соответствовать адресу нашего веб-сервиса (например, /ws/MyProxyService/*).
    • Мы создаем обработчик для метода POST (или других методов, используемых вашим WS) с параметрами Запрос и Ответ.
  2. Перехват и логирование входящего запроса:
    • Внутри обработчика HTTP-сервиса мы получаем доступ к объекту Запрос (тип HttpСервисЗапрос).
    • Через метод Запрос.ПолучитьТелоКакСтроку() мы можем получить полный текст входящего HTTP-запроса, включая его SOAP-содержимое.
    • Этот текст мы можем записать в журнал регистрации 1С, в свой собственный регистр сведений, или в отдельный текстовый файл на сервере 1С.
      
      // Обработчик HTTP-сервиса
      Процедура ОбработатьЗапрос(Запрос, Ответ)
          // 1. Получаем и логируем текст входящего запроса
          ТекстВходящегоЗапроса = "";
          Попытка
              ТекстВходящегоЗапроса = Запрос.ПолучитьТелоКакСтроку();
              // Запись в журнал регистрации 1С
              ЗаписьЖурналаРегистрации(НСтр("ru='Входящий HTTP-запрос'"),
                                       УровеньЖурналаРегистрации.Информация,
                                       ,
                                       ,
                                       "URI: " + Запрос.URI + Символы.ПС +
                                       "Метод: " + Запрос.Метод + Символы.ПС +
                                       "Тело: " + ТекстВходящегоЗапроса);
              // Или запись в регистр сведений, файл и т.д.
          Исключение
              ЗаписьЖурналаРегистрации(НСтр("ru='Ошибка при получении тела запроса'"),
                                       УровеньЖурналаРегистрации.Ошибка,
                                       ,
                                       ,
                                       "URI: " + Запрос.URI + Символы.ПС +
                                       "Метод: " + Запрос.Метод + Символы.ПС +
                                       "Ошибка: " + ОписаниеОшибки());
          КонецПопытки;
      
          // 2. Формируем новый запрос для целевого веб-сервиса 1С
          АдресЦелевогоWS = "http://localhost/MyBase/ws/MyWebService.1cws"; // Укажите реальный адрес вашего WS
          HTTPЗапросКWS = Новый HTTPЗапрос(АдресЦелевогоWS);
          HTTPЗапросКWS.УстановитьТелоИзСтроки(ТекстВходящегоЗапроса, КодировкаТекста.UTF8, "text/xml");
      
          // Копируем заголовки, если это необходимо (например, Content-Type, SOAPAction)
          Для Каждого Заголовок Из Запрос.Заголовки Цикл
              // Мы можем выборочно копировать заголовки, избегая те, что 1С может добавить сам
              Если НЕ Найти("Host,Content-Length,Connection", Заголовок.Ключ) Тогда
                  HTTPЗапросКWS.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);
              КонецЕсли;
          КонецЦикла;
      
          // 3. Отправляем запрос на целевой веб-сервис
          HTTPКлиент = Новый HTTPКлиент();
          Попытка
              HTTPОтветОтWS = HTTPКлиент.Отправить(HTTPЗапросКWS);
      
              // 4. Обрабатываем ответ от целевого веб-сервиса
              Если HTTPОтветОтWS.КодСостояния = 200 Тогда
                  Ответ.УстановитьТелоИзСтроки(HTTPОтветОтWS.ПолучитьТелоКакСтроку(), HTTPОтветОтWS.КодировкаТела, HTTPОтветОтWS.Заголовок("Content-Type"));
                  Для Каждого Заголовок Из HTTPОтветОтWS.Заголовки Цикл
                      Если НЕ Найти("Transfer-Encoding,Content-Length", Заголовок.Ключ) Тогда
                          Ответ.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);
                      КонецЕсли;
                  КонецЦикла;
                  Ответ.КодСостояния = HTTPОтветОтWS.КодСостояния;
              Иначе
                  // Если WS вернул ошибку, мы можем залогировать ее и передать клиенту
                  ТекстОшибкиWS = HTTPОтветОтWS.ПолучитьТелоКакСтроку();
                  ЗаписьЖурналаРегистрации(НСтр("ru='Ошибка от целевого WS'"),
                                           УровеньЖурналаРегистрации.Ошибка,
                                           ,
                                           ,
                                           "URI: " + Запрос.URI + Символы.ПС +
                                           "Код состояния: " + HTTPОтветОтWS.КодСостояния + Символы.ПС +
                                           "Тело ошибки: " + ТекстОшибкиWS);
      
                  Ответ.УстановитьТелоИзСтроки(ТекстОшибкиWS, HTTPОтветОтWS.КодировкаТела, HTTPОтветОтWS.Заголовок("Content-Type"));
                  Ответ.КодСостояния = HTTPОтветОтWS.КодСостояния;
              КонецЕсли;
      
          Исключение
              // Ошибка при отправке HTTP-запроса к целевому WS (например, недоступность)
              ЗаписьЖурналаРегистрации(НСтр("ru='Ошибка отправки запроса к целевому WS'"),
                                       УровеньЖурналаРегистрации.Ошибка,
                                       ,
                                       ,
                                       ОписаниеОшибки());
              Ответ.КодСостояния = 500;
              Ответ.УстановитьТелоИзСтроки("<faultstring>Прокси: Ошибка при обращении к целевому WS</faultstring>", КодировкаТекста.UTF8, "text/xml");
          КонецПопытки;
      
      КонецПроцедуры
      
  3. Публикация HTTP-сервиса:
    • После создания HTTP-сервиса, мы должны его опубликовать на веб-сервере.
    • Важно: Убедитесь, что новый HTTP-сервис опубликован по тому же пути (или префиксу), куда изначально обращался клиент. Например, если клиент обращался к /ws/MyWebService.1cws, то наш HTTP-сервис должен быть опубликован как /ws/MyWebService/* или /ws/MyWebService.1cws (если веб-сервис переименован). При этом исходный веб-сервис 1С нужно будет опубликовать по другому, внутреннему адресу, к которому будет обращаться наш прокси HTTP-сервис.

Преимущества этого подхода:

Недостатки:

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

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

  1. Выборочное логирование: Постоянное логирование полного тела каждого запроса может значительно увеличить объем логов и негативно сказаться на производительности. Мы рекомендуем настраивать логирование тела запроса только для отладки, для запросов, которые приводят к ошибкам, или для определенных критичных URL.
  2. Ограничение размера логов: Убедитесь, что мы настроили ротацию логов (сжатие, удаление старых файлов) на веб-сервере или в нашей системе логирования 1С, чтобы избежать переполнения дискового пространства.
  3. Использование идентификаторов корреляции: Если возможно, используйте идентификаторы корреляции (например, exchangeId в SOAP-заголовках или уникальный ID, генерируемый при получении запроса). Это поможет нам связать запросы и ответы в различных логах (веб-сервера, 1С, сторонних систем) для более простого анализа и трассировки проблем.
  4. Мониторинг производительности: После внедрения любого решения по логированию, мы обязательно должны отслеживать производительность веб-сервера и сервера 1С, чтобы убедиться, что логирование не создает узких мест.

Выбирая между логированием на уровне веб-сервера и использованием HTTP-сервиса 1С как прокси, мы должны учитывать наши возможности по настройке инфраструктуры, необходимость глубокой кастомизации логики и требования к производительности. Оба подхода позволяют нам получить доступ к тексту некорректного WS-запроса и значительно упростить отладку проблем интеграции.

← На главную