P8-ExchangeService/core/http_client.js

237 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Сервис интеграции ПП Парус 8 с WEB API
Модуль ядра: работа с HTTP/HTTPS запросами
*/
//------------------------------
// Подключение внешних библиотек
//------------------------------
const { URL } = require("url");
//--------------------------
// Локальные идентификаторы
//--------------------------
const DEFAULT_TIMEOUT = 30000; //Таймаут по умолчанию
//Выполнение HTTP/HTTPS запроса
const httpRequest = async (rawOptions = {}) => {
try {
//Нормализуем параметры запроса
const options = normalizeOptions(rawOptions);
//Сформируем ссылку
const url = buildURL(options.url, options.query);
//Подготовим заголовки и тело запроса
const headers = prepareHeaders(options.headers);
const { body, contentLength, isStream } = prepareBody({
body: options.body,
json: options.json,
headers
});
//Если не указан размер тела
if (contentLength !== undefined && !headers.has("content-length")) {
//Установим размер тела в заголовок
headers.set("content-length", String(contentLength));
}
//Выполним запрос с использованием fetch
return httpRequestFetch({ options, url, headers, body, isStream });
} catch (e) {
throw e;
}
};
//Ошибка HTTP-запроса
class HttpError extends Error {
constructor(message, response) {
super(message);
this.name = "HttpError";
this.response = response;
}
}
//Выполнение запроса с использованием fetch
const httpRequestFetch = async ({ options, url, headers, body, isStream }) => {
try {
const fetchImpl = globalThis.fetch;
if (typeof fetchImpl !== "function") {
throw new Error("globalThis.fetch недоступен. Используйте Node.js >= 18.");
}
let requestBody = body;
if (isStream && body && typeof body.pipe === "function") {
const chunks = [];
try {
for await (const chunk of body) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
requestBody = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
} catch (streamError) {
throw new Error(`Ошибка чтения потока: ${streamError.message}`);
}
}
const timeoutController = new AbortController();
const signals = [];
if (options.signal && options.signal instanceof AbortSignal) {
signals.push(options.signal);
}
signals.push(timeoutController.signal);
const combinedSignal = AbortSignal.any(signals);
const timeoutId =
options.timeout > 0
? setTimeout(
() => timeoutController.abort(new Error(`Время ожидания выполнения запроса истекло после ${options.timeout} мс`)),
options.timeout
)
: null;
try {
const fetchOptions = {
method: options.method,
headers: Object.fromEntries(headers),
body: requestBody,
signal: combinedSignal,
redirect: options.followRedirects ? "follow" : "manual"
};
if (options.proxy || options.ca || options.cert || options.key || options.passphrase || options.rejectUnauthorized === false) {
const { Agent, ProxyAgent } = require("undici");
const connectOptions = {
rejectUnauthorized: options.rejectUnauthorized !== undefined ? options.rejectUnauthorized : true
};
if (options.ca) {
connectOptions.ca = Array.isArray(options.ca) ? options.ca : [options.ca];
}
if (options.cert) {
connectOptions.cert = options.cert;
}
if (options.key) {
connectOptions.key = options.key;
}
if (options.passphrase) {
connectOptions.passphrase = options.passphrase;
}
if (options.proxy) {
const proxyUrl = new URL(options.proxy);
if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") {
throw new Error("Поддерживаются только HTTP/HTTPS-прокси (protocol=http|https).");
}
const proxyAuth =
proxyUrl.username || proxyUrl.password
? `Basic ${Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString(
"base64"
)}`
: null;
fetchOptions.dispatcher = new ProxyAgent({
uri: options.proxy,
token: proxyAuth,
connect: connectOptions
});
} else {
fetchOptions.dispatcher = new Agent({
connect: connectOptions
});
}
}
const response = await fetchImpl(url.toString(), fetchOptions);
const responseBody = Buffer.from(await response.arrayBuffer());
const result = {
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries()),
body: responseBody,
ok: response.ok,
url: response.url
};
if (options.throwOnErrorStatus && !result.ok) {
throw new HttpError(`Запрос не выполнен со статусом ${result.statusCode}`, result);
}
return result;
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
} catch (e) {
throw e;
}
};
//Нормализация параметров запроса
const normalizeOptions = options => {
if (!options || typeof options !== "object") {
throw new TypeError("options должен быть объектом");
}
if (!options.url) throw new Error("options.url обязателен");
return {
method: (options.method || "GET").toUpperCase(),
url: options.url,
headers: options.headers || {},
query: options.query || options.qs || {},
body: options.body,
json: options.json,
timeout: options.timeout ?? DEFAULT_TIMEOUT,
followRedirects: options.followRedirects ?? false,
proxy: options.proxy || null,
ca: options.ca,
cert: options.cert,
key: options.key,
passphrase: options.passphrase,
rejectUnauthorized: options.rejectUnauthorized !== undefined ? options.rejectUnauthorized : true,
throwOnErrorStatus: options.throwOnErrorStatus ?? false,
signal: options.signal || null
};
};
//Формирование ссылки
const buildURL = (base, query = {}) => {
const url = new URL(base);
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null) return;
url.searchParams.append(key, String(value));
});
return url;
};
//Подготовка заголовков запроса
const prepareHeaders = (inputHeaders = {}) => {
const headers = new Map();
Object.entries(inputHeaders).forEach(([key, value]) => {
if (value === undefined || value === null) return;
headers.set(String(key).toLowerCase(), String(value));
});
return headers;
};
//Подготовка тела запроса
const prepareBody = ({ body, json, headers }) => {
if (json !== undefined) {
const payload = Buffer.from(JSON.stringify(json));
if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
return { body: payload, contentLength: payload.length, isStream: false };
}
if (body === undefined || body === null) return { body: undefined, contentLength: undefined, isStream: false };
if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
return { body, contentLength: body.length, isStream: false };
}
if (typeof body === "string") {
const payload = Buffer.from(body);
return { body: payload, contentLength: payload.length, isStream: false };
}
if (typeof body.pipe === "function") {
return { body, contentLength: undefined, isStream: true };
}
const payload = Buffer.from(JSON.stringify(body));
if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
return { body: payload, contentLength: payload.length, isStream: false };
};
exports.httpRequest = httpRequest;
exports.HttpError = HttpError;