From bda18e40eca451d65c06cc18209be6ebec15a5a2 Mon Sep 17 00:00:00 2001 From: boa604 Date: Tue, 21 Apr 2026 14:41:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20HTTP/HTTPS=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=D0=BC=D0=B8=20=D0=B2=20?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D0=B8=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D1=8F=D1=8E=D1=89=D0=B8=D1=85=D1=81=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/http_client.js | 168 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 141 insertions(+), 27 deletions(-) diff --git a/core/http_client.js b/core/http_client.js index dc088e7..9dda490 100644 --- a/core/http_client.js +++ b/core/http_client.js @@ -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