forked from CITKParus/P8-ExchangeService
Доработка модуля работы с HTTP/HTTPS запросами в части повторяющихся заголовков
This commit is contained in:
parent
336804b171
commit
bda18e40ec
@ -18,6 +18,8 @@ const { Socket } = require("net"); //Встроенная поддержка с
|
|||||||
|
|
||||||
//Таймаут по умолчанию
|
//Таймаут по умолчанию
|
||||||
const DEFAULT_TIMEOUT = 30000;
|
const DEFAULT_TIMEOUT = 30000;
|
||||||
|
//Заголовки, которые нельзя объединять
|
||||||
|
const NON_COMBINABLE_HEADERS = new Set(["set-cookie"]);
|
||||||
|
|
||||||
//Ошибка HTTP-запроса
|
//Ошибка HTTP-запроса
|
||||||
class HttpError extends Error {
|
class HttpError extends Error {
|
||||||
@ -144,6 +146,106 @@ const prepareBody = ({ body, jsonRequest, headers }) => {
|
|||||||
return { body: payload, contentLength: payload.length, isStream: false };
|
return { body: payload, contentLength: payload.length, isStream: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Добавление значения в коллекцию
|
||||||
|
const appendRepeatedValue = (target, key, value) => {
|
||||||
|
if (value === undefined) return;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
||||||
|
const currentValue = target[key];
|
||||||
|
if (Array.isArray(currentValue)) {
|
||||||
|
currentValue.push(value);
|
||||||
|
} else {
|
||||||
|
target[key] = [currentValue, value];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Удаление заголовка из массива сырых заголовков
|
||||||
|
const removeHeaderFromRawPairs = (rawHeaders, headerName) => {
|
||||||
|
for (let i = 0; i + 1 < rawHeaders.length; i += 2) {
|
||||||
|
if (String(rawHeaders[i]).toLowerCase() === headerName) {
|
||||||
|
rawHeaders.splice(i, 2);
|
||||||
|
i -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Переопределение значений заголовка
|
||||||
|
const applyHeaderValues = (headers, rawHeaders, headerName, values) => {
|
||||||
|
if (!Array.isArray(values) || values.length === 0) return;
|
||||||
|
headers[headerName] = values.slice();
|
||||||
|
removeHeaderFromRawPairs(rawHeaders, headerName);
|
||||||
|
for (const value of values) {
|
||||||
|
rawHeaders.push(headerName, String(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Формирование заголовков из массива rawHeaders/rawTrailers
|
||||||
|
const buildHeadersFromRawPairs = rawPairs => {
|
||||||
|
const headers = {};
|
||||||
|
const rawHeaders = Array.isArray(rawPairs) ? rawPairs.slice() : [];
|
||||||
|
for (let i = 0; i + 1 < rawHeaders.length; i += 2) {
|
||||||
|
const headerName = String(rawHeaders[i] || "").trim();
|
||||||
|
const headerValue = String(rawHeaders[i + 1] || "").trim();
|
||||||
|
if (!headerName) continue;
|
||||||
|
appendRepeatedValue(headers, headerName.toLowerCase(), headerValue);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
rawHeaders
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
//Формирование заголовков из fetch Headers
|
||||||
|
const buildHeadersFromFetchHeaders = responseHeaders => {
|
||||||
|
const headers = {};
|
||||||
|
const rawHeaders = [];
|
||||||
|
for (const [name, value] of responseHeaders.entries()) {
|
||||||
|
const key = String(name || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!key) continue;
|
||||||
|
appendRepeatedValue(headers, key, value);
|
||||||
|
rawHeaders.push(name, value);
|
||||||
|
}
|
||||||
|
//Обработка некомбинируемых заголовков
|
||||||
|
if (NON_COMBINABLE_HEADERS.has("set-cookie") && typeof responseHeaders.getSetCookie === "function") {
|
||||||
|
const setCookieValues = responseHeaders.getSetCookie();
|
||||||
|
applyHeaderValues(headers, rawHeaders, "set-cookie", setCookieValues);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
rawHeaders
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
//Парсинг стартовой строки и заголовков HTTP ответа
|
||||||
|
const parseHttpHeaderSection = headerText => {
|
||||||
|
const lines = headerText.split("\r\n");
|
||||||
|
const statusLine = lines[0] || "";
|
||||||
|
const statusMatch = statusLine.match(/HTTP\/[\d.]+\s+(\d+)\s*(.*)$/);
|
||||||
|
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 200;
|
||||||
|
const statusMessage = statusMatch ? statusMatch[2] || "" : "";
|
||||||
|
const rawHeaderPairs = [];
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const colonIndex = line.indexOf(":");
|
||||||
|
if (colonIndex === -1) continue;
|
||||||
|
const name = line.substring(0, colonIndex).trim();
|
||||||
|
const value = line.substring(colonIndex + 1).trim();
|
||||||
|
if (!name) continue;
|
||||||
|
rawHeaderPairs.push(name, value);
|
||||||
|
}
|
||||||
|
const { headers, rawHeaders } = buildHeadersFromRawPairs(rawHeaderPairs);
|
||||||
|
return {
|
||||||
|
statusCode,
|
||||||
|
statusMessage,
|
||||||
|
headers,
|
||||||
|
rawHeaders
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
//Обработка ответа (парсинг JSON, форматирование для совместимости с "Request" и "Request-Promice")
|
//Обработка ответа (парсинг JSON, форматирование для совместимости с "Request" и "Request-Promice")
|
||||||
const processResponse = (result, options) => {
|
const processResponse = (result, options) => {
|
||||||
//Буфер для обработки тела
|
//Буфер для обработки тела
|
||||||
@ -164,13 +266,13 @@ const processResponse = (result, options) => {
|
|||||||
//Формируем результат в зависимости от "resolveWithFullResponse" (это параметр из "Request": true - отвечать с заголовком, false - отвечать сразу телом)
|
//Формируем результат в зависимости от "resolveWithFullResponse" (это параметр из "Request": true - отвечать с заголовком, false - отвечать сразу телом)
|
||||||
if (options.resolveWithFullResponse) {
|
if (options.resolveWithFullResponse) {
|
||||||
//Просили ответить полным ответом
|
//Просили ответить полным ответом
|
||||||
return {
|
const fullResponse = { ...result };
|
||||||
statusCode: result.statusCode,
|
fullResponse.statusCode = result.statusCode;
|
||||||
statusMessage: result.statusMessage || "",
|
fullResponse.statusMessage = result.statusMessage || "";
|
||||||
headers: result.headers,
|
fullResponse.headers = result.headers;
|
||||||
body: processedBody,
|
fullResponse.body = processedBody;
|
||||||
url: result.url
|
fullResponse.url = result.url;
|
||||||
};
|
return fullResponse;
|
||||||
} else {
|
} else {
|
||||||
//Просили только тело
|
//Просили только тело
|
||||||
return processedBody;
|
return processedBody;
|
||||||
@ -253,11 +355,17 @@ const httpRequestNative = (options, url, headers, body) => {
|
|||||||
res.on("end", () => {
|
res.on("end", () => {
|
||||||
//Собираем тело как бинарный буфер
|
//Собираем тело как бинарный буфер
|
||||||
const responseBody = Buffer.concat(chunks);
|
const responseBody = Buffer.concat(chunks);
|
||||||
|
const { headers: normalizedHeaders, rawHeaders } = buildHeadersFromRawPairs(res.rawHeaders);
|
||||||
|
const { headers: normalizedTrailers, rawHeaders: rawTrailers } = buildHeadersFromRawPairs(res.rawTrailers);
|
||||||
//Формируем объект ответа в стиле "Request" для совметимости
|
//Формируем объект ответа в стиле "Request" для совметимости
|
||||||
const result = {
|
const result = {
|
||||||
statusCode: res.statusCode,
|
statusCode: res.statusCode,
|
||||||
statusMessage: res.statusMessage || "",
|
statusMessage: res.statusMessage || "",
|
||||||
headers: res.headers,
|
headers: normalizedHeaders,
|
||||||
|
rawHeaders,
|
||||||
|
trailers: normalizedTrailers,
|
||||||
|
rawTrailers,
|
||||||
|
httpVersion: res.httpVersion,
|
||||||
body: responseBody,
|
body: responseBody,
|
||||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||||
url: url
|
url: url
|
||||||
@ -393,7 +501,9 @@ const httpRequestThroughProxy = (requestOptions, targetUrl, headers, body, proxy
|
|||||||
//Готовим буфер для чтения ответа
|
//Готовим буфер для чтения ответа
|
||||||
let responseBuffer = Buffer.alloc(0);
|
let responseBuffer = Buffer.alloc(0);
|
||||||
let responseHeaders = {};
|
let responseHeaders = {};
|
||||||
|
let responseRawHeaders = [];
|
||||||
let statusCode = 200;
|
let statusCode = 200;
|
||||||
|
let statusMessage = "";
|
||||||
let headerParsed = false;
|
let headerParsed = false;
|
||||||
let contentLength = -1;
|
let contentLength = -1;
|
||||||
//Функция для наполнения и обработки буфера ответа
|
//Функция для наполнения и обработки буфера ответа
|
||||||
@ -412,23 +522,18 @@ const httpRequestThroughProxy = (requestOptions, targetUrl, headers, body, proxy
|
|||||||
const bodyStart = headerEnd + 4;
|
const bodyStart = headerEnd + 4;
|
||||||
responseBuffer = responseBuffer.subarray(bodyStart);
|
responseBuffer = responseBuffer.subarray(bodyStart);
|
||||||
//Разбираем полученный текст заголовка
|
//Разбираем полученный текст заголовка
|
||||||
const lines = headerText.split("\r\n");
|
const parsedHeader = parseHttpHeaderSection(headerText);
|
||||||
const statusLine = lines[0];
|
statusCode = parsedHeader.statusCode;
|
||||||
const statusMatch = statusLine.match(/HTTP\/[\d.]+\s+(\d+)/);
|
statusMessage = parsedHeader.statusMessage;
|
||||||
if (statusMatch) {
|
responseHeaders = parsedHeader.headers;
|
||||||
statusCode = parseInt(statusMatch[1], 10);
|
responseRawHeaders = parsedHeader.rawHeaders;
|
||||||
}
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
const contentLengthHeader = responseHeaders["content-length"];
|
||||||
const line = lines[i];
|
const contentLengthValue = Array.isArray(contentLengthHeader)
|
||||||
const colonIndex = line.indexOf(":");
|
? contentLengthHeader[contentLengthHeader.length - 1]
|
||||||
if (colonIndex !== -1) {
|
: contentLengthHeader;
|
||||||
const key = line.substring(0, colonIndex).trim().toLowerCase();
|
if (contentLengthValue !== undefined) {
|
||||||
const value = line.substring(colonIndex + 1).trim();
|
contentLength = parseInt(contentLengthValue, 10);
|
||||||
responseHeaders[key] = value;
|
|
||||||
if (key === "content-length") {
|
|
||||||
contentLength = parseInt(value, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//И поднимаем флаг его разобранности
|
//И поднимаем флаг его разобранности
|
||||||
headerParsed = true;
|
headerParsed = true;
|
||||||
@ -460,8 +565,12 @@ const httpRequestThroughProxy = (requestOptions, targetUrl, headers, body, proxy
|
|||||||
//Формируем объект с результатами обработки соединения, совместимый с ожиданиями сервера приложений
|
//Формируем объект с результатами обработки соединения, совместимый с ожиданиями сервера приложений
|
||||||
const result = {
|
const result = {
|
||||||
statusCode: statusCode,
|
statusCode: statusCode,
|
||||||
statusMessage: "",
|
statusMessage: statusMessage,
|
||||||
headers: responseHeaders,
|
headers: responseHeaders,
|
||||||
|
rawHeaders: responseRawHeaders,
|
||||||
|
trailers: {},
|
||||||
|
rawTrailers: [],
|
||||||
|
httpVersion: "1.1",
|
||||||
body: responseBuffer,
|
body: responseBuffer,
|
||||||
ok: statusCode >= 200 && statusCode < 300,
|
ok: statusCode >= 200 && statusCode < 300,
|
||||||
url: targetUrl.toString()
|
url: targetUrl.toString()
|
||||||
@ -576,11 +685,16 @@ const httpRequest = async (rawOptions = {}) => {
|
|||||||
const response = await fetchImpl(url.toString(), fetchOptions);
|
const response = await fetchImpl(url.toString(), fetchOptions);
|
||||||
//Читаем тело ответа
|
//Читаем тело ответа
|
||||||
const responseBody = Buffer.from(await response.arrayBuffer());
|
const responseBody = Buffer.from(await response.arrayBuffer());
|
||||||
|
const { headers: normalizedHeaders, rawHeaders } = buildHeadersFromFetchHeaders(response.headers);
|
||||||
//Собираем ответ в формате, совместимом с сервисом интеграции
|
//Собираем ответ в формате, совместимом с сервисом интеграции
|
||||||
const result = {
|
const result = {
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
statusMessage: response.statusText || "",
|
statusMessage: response.statusText || "",
|
||||||
headers: Object.fromEntries(response.headers.entries()),
|
headers: normalizedHeaders,
|
||||||
|
rawHeaders,
|
||||||
|
trailers: {},
|
||||||
|
rawTrailers: [],
|
||||||
|
httpVersion: "1.1",
|
||||||
body: responseBody,
|
body: responseBody,
|
||||||
ok: response.ok,
|
ok: response.ok,
|
||||||
url: response.url
|
url: response.url
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user