Доработка модуля работы с HTTP/HTTPS запросами в части повторяющихся заголовков
This commit is contained in:
parent
336804b171
commit
bda18e40ec
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user