-
Notifications
You must be signed in to change notification settings - Fork 17
this.http
Аналогично this.db, this.http — не фиксированное имя переменной (в отличие от this.user и ей подобных), а типовое обозначение ресурса определённой природы: HTTP-клиента. Точнее, тонкой обёртки над родным HTTP-клиентом node.js: http.request ().
Класс 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.
Для HTTP-запросов и ответов, требующих поточной обработки, предназначен низкоуровневый метод
response_promise = responseStream (options, body)
Он принимает на вход 2 параметра:
-
options(обязательный) — набор опций, переопределяемых по отношению к конфигурации (pathи т. п.) -
body(необязательный) — тело запроса:- либо строка;
- либо экземпляр 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-запрос, тело которого формируется из массива записей 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-, так и 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-сервисами отправка запроса по сети на нужный адрес и ожидание ответа -- это полдела. В отличие от 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.