diff --git a/lib/core/runtime/capture/index.ts b/lib/core/runtime/capture/index.ts index 312eabc7..93a11173 100644 --- a/lib/core/runtime/capture/index.ts +++ b/lib/core/runtime/capture/index.ts @@ -29,41 +29,53 @@ const SENSITIVE_HEADERS = new Set([ ]); /** - * Mask sensitive header values in a header string. - * e.g. "cookie: abc123" => "cookie: ***" + * Parse a raw HTTP header string into an object, + * masking sensitive header values with "***". */ -const maskSensitiveHeaders = (headerStr: string): string => { - if (!headerStr) return headerStr; - - return headerStr.replace( - /^([^:]+):\s*(.+)$/gm, - (match, key: string) => { - if (SENSITIVE_HEADERS.has(key.trim().toLowerCase())) { - return `${key}: ***`; - } - - return match; +const parseHeaders = (str: string): Record | undefined => { + if (!str) return undefined; + const obj: Record = {}; + + str.trim().split(/\r?\n/).forEach((line) => { + const idx = line.indexOf(":"); + if (idx === -1) { + obj._firstLine = line; + } else { + const key = line.slice(0, idx).trim(); + obj[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) + ? "***" + : line.slice(idx + 1).trim(); } - ); -}; + }); -/** - * Create a sanitized copy of requestLog for safe logging. - */ -const sanitizeRequestLog = ( - log: Partial -): Partial => { - const sanitized = { ...log }; - - if (sanitized.requestHeader) { - sanitized.requestHeader = maskSensitiveHeaders(sanitized.requestHeader); - } + return obj; +}; - if (sanitized.responseHeader) { - sanitized.responseHeader = maskSensitiveHeaders(sanitized.responseHeader); +/** Decode a base64-encoded body, truncated to maxLen characters. */ +const decodeBody = (base64Str: string | undefined, maxLen = 2000): string | null => { + if (!base64Str) return null; + try { + const text = Buffer.from(base64Str, "base64").toString("utf-8"); + + return text.length > maxLen + ? `${text.slice(0, maxLen)}... (truncated, total ${text.length} chars)` + : text; + } catch { + return null; } +}; - return sanitized; +const formatRequestLog = (log: Partial): string => { + const resBody = decodeBody(log.responseBody); + const info = JSON.stringify({ + url: `${log.protocol} ${log.host}${log.path}`, + statusCode: log.statusCode, + requestHeader: parseHeaders(log.requestHeader), + responseHeader: parseHeaders(log.responseHeader), + responseLength: log.responseLength + }, null, 2); + + return resBody ? `${info}\nResponseBody: ${resBody}` : info; }; /** @@ -181,7 +193,7 @@ export const hack = ( if (err) { logger.error(`${logPre} Lookup ${host} -> ${address || "null"}, error ${err.stack}`); - logger.error(`${logPre} Request: ${JSON.stringify(sanitizeRequestLog(requestLog))}`); + logger.debug(`${logPre} Request: ${formatRequestLog(requestLog)}`); } }); } @@ -206,9 +218,21 @@ export const hack = ( }); request.once("error", (error: Error) => { + // error may fire before finish — manually capture headers & body + if (!requestLog.requestHeader) { + requestLog.requestHeader = (request as any)._header; + } + + if (!requestLog.requestBody) { + const body = (request as any)._body; + if (body) { + requestLog.requestBody = body.toString("base64"); + } + } + logger.error(`${logPre} Request error. Stack: ${error.stack}`); - logger.error(`${logPre} Request: ${JSON.stringify(sanitizeRequestLog(requestLog))}`); + logger.error(`${logPre} Request: ${formatRequestLog(requestLog)}`); finishRequest(); clearDomain(); @@ -304,6 +328,11 @@ export const hack = ( timestamps.responseClose - timestamps.onSocket } ms`); + // Log full request/response details for non-2xx to aid debugging + if (response.statusCode >= 400) { + logger.debug(`${logPre} ❌ HTTP request failed [${response.statusCode}]: ${formatRequestLog(requestLog)}`); + } + finishRequest(); }); });