Skip to content
This repository was archived by the owner on Mar 2, 2025. It is now read-only.

this.http

do- edited this page Dec 1, 2022 · 53 revisions

Аналогично this.db, this.http — не фиксированное имя переменной (в отличие от this.user и ей подобных), а типовое обозначение ресурса определённой природы: HTTP-клиента. Точнее, тонкой обёртки над родным HTTP-клиентом node.js: http.request ().

Table of Contents

Конфигурация

Класс HTTP предназначен для хранения пакета параметров, переиспользуемых от вызова к вызову http.request (). Во избежание разночтений, отметим: он не реализует функциональности хранения соединений (connection pooling): эта задача успешно решена стандартным http.Agent.

Что же, собственно, делает объект, позиционируемый как HTTP-клиент в Dia.js? Прежде всего, он хранит переданные при инициализации опции запроса -- поддерживаемые http.request () и дополнительно ещё опцию url:

  this.pools = {
    //...        
      http_some_soap_svc: new HTTP ({
        url: this.ws.some_soap_svc.url,
        timeout: 100,            // например
        agent: http.globalAgent, // по умолчанию
        //...        
      }),
      http_my_rest_svc: new HTTP ({
        url: this.ws.my_rest_svc.url,
        //...        
      }),
    //...        
  }

Если задан url, он определяет protocol, hostname, port и path. Любую из этих и прочих опций можно переопределять при выполнении запроса. Предполагается, что в основном это должен быть только path.

Общий случай: метод responseStream

Для HTTP-запросов и ответов, требующих поточной обработки, предназначен низкоуровневый метод

 response_promise = responseStream (options, body)

Он принимает на вход 2 параметра:

  • options (обязательный) — набор опций, переопределяемых по отношению к конфигурации (path и т. п.)
  • body (необязательный) — тело запроса:
На выходе — Promise, возвращающий тот же объект типа http.IncomingMessage, который можно получить стандартным http.request. То есть, помимо возможности читать и перенаправлять его как stream.Readable, у этого объекта можно работать с полями statusCode, headers и т. п.

Отличия от стандартного API node.js следующие:

  • выходной поток доступен не внутри callback-функции, а на выходе асинхронного метода;
  • для него установлен обработчик события error — так, что при вызове this.http.responseStream внутри try-блока будет сгенерировано исключение, попадущее в соответствующий catch-блок;
  • часть параметров берется из конфигурации (см. выше);
  • если задано body, то options.method принимается по умолчанию POST, иначе GET
    • если body задано в виде строки, а headers не определены, то Content-Type определяется по первому символу тела:
      • для <text/xml;
      • для { или [application/json;
      • иначе — text/plain.
  • в зависимости от опции protocol (в том числе вычисленной по url) подставляется библиотека http или https (интересно: в API node.js за этот выбор отвечает не платформа, а приложение);
  • при получении ответа с заголовком location производится переход по указанному адресу (возможно, несколько раз), а на выход выдаётся тело последнего ответа в цепочке;
  • при обнаружении 'content-encoding'='gzip' результирующий поток автоматически распаковывается (подменяется на zlib.createGunzip с копированием полей statusCode и headers).

Формирование тела POST в поточном режиме

В примере ниже показано, как отправить POST-запрос, тело которого формируется из массива записей data по ходу отправки. Исходный массив находится в памяти, однако его текстовая копия присутствует там не целиком, а отправляется построчно.

  let body = new (require ('stream')).PassThrough ()
  let res_promise = this.backend.response ({path}, body)
  for (let r of data) {
    let d = {}; for (let k of fields) d [k] = r [k]	
    body.write (JSON.stringify (d) + '\n')
  }
  body.end ()
  await res_promise

Отметим последовательность действий:

  • создание потока-переходника (stream.PassThrough)
  • создание Promise для HTTP-ответа (await в этом месте применять нельзя, поскольку тело ещё не отправлено)
  • запись строк в переходник:
    • приложение видит его как поток для записи;
    • а this.backend.response — как поток для чтения;
  • наконец, после закрытия переходника (body.end ()) — ожидание HTTP-ответа.

Скачивание больших файлов: чтение принятого потока

В следующем примере показано, как реализовать скачивание файла неограниченной длины с отслеживанием процесса посредством progress-stream:

let os = fs.createWriteStream (tmpfn)

try {

  let rp  = await this.http.responseStream ({path: "/some/huge/file.zip"})

  let len = rp.headers ['content-length']

  let meter = require ('progress-stream') ({len, time: 5000}, async p => {
    let {percentage, eta} = p
    await this.fork ({action: 'update'}, {data: {percentage, eta}})
  })

  await new Promise ((ok, fail) => {
    rp.on ('end', ok).pipe (meter).pipe (os)
  })
  
}
finally {
  os.close ()
}

SOAP/REST-JSON: метод response

Для подавляющего большинства точек интеграции (как SOAP-, так и RESTful-сервисов) размеры HTTP-ответов имеют жёсткие ограничения сверху (например, 1 Мб), позволяющие размещать их в памяти без риска переполнения.

В таких случаях гораздо удобнее работать не с потоками, а со строками — для этого педназначен метод response. Его реализация сводится к:

  • вызову responseStream;
  • чтению тела ответа в строку;
  • возврату:
    • для ответа с кодом 200 OK — самой строки в качестве ответа;
    • в прочих ситуациях — ошибки (см. ниже).
  let rp = await this.http_my_rest_svc.response (
    {path: "/1.1/rest/resource/somePath"},       // дополнительные опции
    this.rq.data                                 // тело запроса
  )

  let rp = await this.http_my_rest_svc.response (
    {path: "/1.1/rest/resource/otherPath"},      // ... а тела нет
  )

  let soap_response = await this.http_some_soap_svc.response (
    {},                                          // все опции -- в настройках
    this.rq.data                                 // тело запроса
  )

Подробнее о SOAP

При работе с SOAP-сервисами отправка запроса по сети на нужный адрес и ожидание ответа -- это полдела. В отличие от RESTful-сервисов (в обиходном понимании), здесь имеется дополнительная трудоёмкость, связанная с формированием XML по схеме и добавлением HTTP-заголовков (в частности, SOAPaction) в соответствии с WSDL.

В Dia.js нет каких-либо специальных инструментов для работы с XML вообще и SOAP в частности, но автор предлагает воспользоваться для этих целей отдельным модулем node-xml-toolkit. В частности, для вызова Web-методов SOAP 1.1 предназначен класс SOAP11.

Вот как выглядит использование этого механизма, по шагам.

1. Проверить back/package.json на предмет "xml-toolkit": "^1.0.13" или выше. Если нет -- добавить и выполнить npm i.

2. В модуле импортировать класс SOAP11:

 // const {SOAP11} = require ('../node_modules/xml-toolkit') // из-под основного back/lib
 // const {SOAP11} = require ('../../../../../back/node_modules/xml-toolkit') // из-под slices/{slice}/back/lib

3. Создать экземпляр SOAP11, загрузив WSDL:

 const soap = await SOAP11.fromFile (Path.join (__dirname, '..', 'Static', 'some.wsdl'))

4. Сгенерировать комплект HTTP-заголовков и тело POST-запроса по объекту данных...

 const {headers, body} = soap.http ({RequestForSomething: {INN: 1111111117, amount: '0.01'}})

5. ...и он готов к отправке вышеописанным методом response

 const soap_rp = await this.http_some_soap_svc.response ({headers}, body)

Объект данных в п. 4 должен соответствовать по структуре XML-запросу, описанному в схеме, заложенной в WSDL. В частности, имя его единственного корневого свойства (в нашем примере это RequestForSomething) должно быть локальным именем элемента единственной части входного SOAP-сообщения нужной операции.

Естественно, всё это работает только в том случае, когда все нужные объекты в WSDL имеют уникальные локальные имена. Если попадётся патологический WSDL с десятком запросов, элементы которых все будут именоваться Request, но лежать в разных пространствах или если внутри Request найдётся атрибут с именем в точности, как у дочернего элемента -- SOAP11, увы, не подойдёт и придётся искать что-то другое либо всё-таки формировать запрос вручную.

Столь же бесполезен SOAP11 для работы с SOAP 1.2. Но поддержка этой спецификации будет добавлена сразу, как только выявится необходимость. Это может показаться странным, однако несмотря на то, что SOAP 1.2 -- собственно, единственный по-настоящему утверждённый стандарт, в то время как SOAP 1.1 навсегда остался с припиской "DRAFT" -- тем не менее, пока на практике автору попадаются исключительно такие "черновые" (изредка -- двухверсионные) сервисы.

Подробнее об ошибках

Для ответов с кодами, отличными от 200 и 201, метод response выбрасывает ошибку типа HTTP/Error. Факт получения аварийного ответа HTTP (в отличие от внутренних проблем) можно определить, например, так:

const HTTPError = require ('../../Ext/Dia/HTTP/Error.js')
try {
  await http.response (//...
}
catch (e) {
  if (e instanceof HTTPError) {
    //...
  }
  else {
    //...
  } 
}
Имя Описание Пример Примечание
code Код статуса HTTP 500
status Код + краткое (обычно стандартное) описание статуса HTTP 500 Internal Server Error Обычно неинформативно, так как полностью определяется кодом
body Тело HTTP-ответа NullPointerException at ... Может отсутствовать
parent Событие протоколирования, описывающее отправку HTTP-запроса

Значение главного для ошибки поля: message — определяется эвристически:

  • если тело сообщения представляет собой SOAP Fault, то в message извлекается внутренность элемента faultstring;
  • иначе если тело не пусто, оно копируется в message целиком;
  • иначе message приравнивается status.

Прочее

Прерывание запроса

Будучи ресурсом, по аналогии с this.db, наш HTTP-клиент реализует метод break ().

Он принудительно прерывает все ранее запущенные HTTP[S]-запросы, для которых это возможно. Реализовано это через попытку вызова destroy () для .socket каждого из созданных запросов.

Вызов этого метода происходит во время:

  • вызова break () обработчика — если среди его ресурсов есть HTTP-клиенты;
  • вызова break () для соединения с БД ClickHouse.
Он может использоваться и напрямую в прикладном коде, однако здесь сложно привести содержательный и притом краткий пример. Это могло бы быть ограничение времени запроса, но здесь гораздо лучше использовать стандартную опцию timeout.

Clone this wiki locally