Доработка модуля работы с HTTP/HTTPS запросами в части повторяющихся заголовков

This commit is contained in:
boa604 2026-04-21 14:41:34 +03:00
parent 336804b171
commit bda18e40ec

View File

@ -18,6 +18,8 @@ const { Socket } = require("net"); //Встроенная поддержка с
//Таймаут по умолчанию
const DEFAULT_TIMEOUT = 30000;
//Заголовки, которые нельзя объединять
const NON_COMBINABLE_HEADERS = new Set(["set-cookie"]);
//Ошибка HTTP-запроса
class HttpError extends Error {
@ -144,6 +146,106 @@ const prepareBody = ({ body, jsonRequest, headers }) => {
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")
const processResponse = (result, options) => {
//Буфер для обработки тела
@ -164,13 +266,13 @@ const processResponse = (result, options) => {
//Формируем результат в зависимости от "resolveWithFullResponse" (это параметр из "Request": true - отвечать с заголовком, false - отвечать сразу телом)
if (options.resolveWithFullResponse) {
//Просили ответить полным ответом
return {
statusCode: result.statusCode,
statusMessage: result.statusMessage || "",
headers: result.headers,
body: processedBody,
url: result.url
};
const fullResponse = { ...result };
fullResponse.statusCode = result.statusCode;
fullResponse.statusMessage = result.statusMessage || "";
fullResponse.headers = result.headers;
fullResponse.body = processedBody;
fullResponse.url = result.url;
return fullResponse;
} else {
//Просили только тело
return processedBody;
@ -253,11 +355,17 @@ const httpRequestNative = (options, url, headers, body) => {
res.on("end", () => {
//Собираем тело как бинарный буфер
const responseBody = Buffer.concat(chunks);
const { headers: normalizedHeaders, rawHeaders } = buildHeadersFromRawPairs(res.rawHeaders);
const { headers: normalizedTrailers, rawHeaders: rawTrailers } = buildHeadersFromRawPairs(res.rawTrailers);
//Формируем объект ответа в стиле "Request" для совметимости
const result = {
statusCode: res.statusCode,
statusMessage: res.statusMessage || "",
headers: res.headers,
headers: normalizedHeaders,
rawHeaders,
trailers: normalizedTrailers,
rawTrailers,
httpVersion: res.httpVersion,
body: responseBody,
ok: res.statusCode >= 200 && res.statusCode < 300,
url: url
@ -393,7 +501,9 @@ const httpRequestThroughProxy = (requestOptions, targetUrl, headers, body, proxy
//Готовим буфер для чтения ответа
let responseBuffer = Buffer.alloc(0);
let responseHeaders = {};
let responseRawHeaders = [];
let statusCode = 200;
let statusMessage = "";
let headerParsed = false;
let contentLength = -1;
//Функция для наполнения и обработки буфера ответа
@ -412,23 +522,18 @@ const httpRequestThroughProxy = (requestOptions, targetUrl, headers, body, proxy
const bodyStart = headerEnd + 4;
responseBuffer = responseBuffer.subarray(bodyStart);
//Разбираем полученный текст заголовка
const lines = headerText.split("\r\n");
const statusLine = lines[0];
const statusMatch = statusLine.match(/HTTP\/[\d.]+\s+(\d+)/);
if (statusMatch) {
statusCode = parseInt(statusMatch[1], 10);
}
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
const colonIndex = line.indexOf(":");
if (colonIndex !== -1) {
const key = line.substring(0, colonIndex).trim().toLowerCase();
const value = line.substring(colonIndex + 1).trim();
responseHeaders[key] = value;
if (key === "content-length") {
contentLength = parseInt(value, 10);
}
}
const parsedHeader = parseHttpHeaderSection(headerText);
statusCode = parsedHeader.statusCode;
statusMessage = parsedHeader.statusMessage;
responseHeaders = parsedHeader.headers;
responseRawHeaders = parsedHeader.rawHeaders;
const contentLengthHeader = responseHeaders["content-length"];
const contentLengthValue = Array.isArray(contentLengthHeader)
? contentLengthHeader[contentLengthHeader.length - 1]
: contentLengthHeader;
if (contentLengthValue !== undefined) {
contentLength = parseInt(contentLengthValue, 10);
}
//И поднимаем флаг его разобранности
headerParsed = true;
@ -460,8 +565,12 @@ const httpRequestThroughProxy = (requestOptions, targetUrl, headers, body, proxy
//Формируем объект с результатами обработки соединения, совместимый с ожиданиями сервера приложений
const result = {
statusCode: statusCode,
statusMessage: "",
statusMessage: statusMessage,
headers: responseHeaders,
rawHeaders: responseRawHeaders,
trailers: {},
rawTrailers: [],
httpVersion: "1.1",
body: responseBuffer,
ok: statusCode >= 200 && statusCode < 300,
url: targetUrl.toString()
@ -576,11 +685,16 @@ const httpRequest = async (rawOptions = {}) => {
const response = await fetchImpl(url.toString(), fetchOptions);
//Читаем тело ответа
const responseBody = Buffer.from(await response.arrayBuffer());
const { headers: normalizedHeaders, rawHeaders } = buildHeadersFromFetchHeaders(response.headers);
//Собираем ответ в формате, совместимом с сервисом интеграции
const result = {
statusCode: response.status,
statusMessage: response.statusText || "",
headers: Object.fromEntries(response.headers.entries()),
headers: normalizedHeaders,
rawHeaders,
trailers: {},
rawTrailers: [],
httpVersion: "1.1",
body: responseBody,
ok: response.ok,
url: response.url