Доработка модуля работы с 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 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