Skip to content

Commit bab9c9c

Browse files
committed
feat: 添加安全日志输出,防止大响应体淹没控制台 (Issue #21)
**核心功能**: - 新增 src/helpers/logger.js 提供 safeLogResponse 纯函数 - 响应体超过 10KB 时自动截断,保留头尾各 512 字符 - 显示大小提示和截断提示信息 **修改位置**: - background.js: 导入 helper,在响应日志和 debug_log 转发中使用 - index.js: 添加 fallback,在调试日志中使用 safeLogResponse - content-script.js: 在 helper 加载链中添加 logger.js - manifest.json: 添加 logger.js 到 web_accessible_resources **测试覆盖**: - 新增 3 个测试用例验证截断逻辑 - 71 个测试全部通过 - Lint: 0 errors **效果**: - 控制台不再被大响应体切割 - 仍能看到关键信息(头尾预览 + 大小提示) - 用户可通过提示了解如何获取完整内容 **已知限制**: - Network 面板问题未解决(Chrome 扩展架构限制) - 需要 chrome.debugger API 或自建请求历史 UI(后续优化) Addresses #21
1 parent b6e96cf commit bab9c9c

File tree

8 files changed

+244
-7
lines changed

8 files changed

+244
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ temp/
3535
*.crx
3636
key.pem
3737
extension.zip
38+
cross-request-store*.zip

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77

88
## [未发布]
99

10-
_当前没有未发布的变更_
10+
### 改进
11+
12+
- **日志安全** - 控制台输出不会被大响应体淹没
13+
- 新增 `src/helpers/logger.js` 提供 `safeLogResponse`
14+
- background/index 使用安全日志助手,输出超过 10KB 时自动截断并提示
15+
- 避免 content-script 控制台被巨型响应体切割,便于调试
16+
- 新增单元测试覆盖截断逻辑,防止回归
1117

1218
## [4.5.0] - 2025-10-17
1319

@@ -386,4 +392,3 @@ _当前没有未发布的变更_
386392
[4.2.0]: https://github.com/leeguooooo/cross-request-master/compare/v4.1.0...v4.2.0
387393
[4.1.0]: https://github.com/leeguooooo/cross-request-master/compare/v4.0.1...v4.1.0
388394
[4.0.1]: https://github.com/leeguooooo/cross-request-master/releases/tag/v4.0.1
389-

background.js

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,60 @@
11
'use strict';
22

3+
let safeLogResponse = null;
4+
5+
try {
6+
/* global importScripts */
7+
importScripts('src/helpers/logger.js');
8+
if (self.CrossRequestHelpers && self.CrossRequestHelpers.safeLogResponse) {
9+
safeLogResponse = self.CrossRequestHelpers.safeLogResponse;
10+
}
11+
} catch (helperError) {
12+
console.warn('[Background] safeLogResponse helper 加载失败:', helperError);
13+
}
14+
15+
if (!safeLogResponse) {
16+
safeLogResponse = function (originalBody, options) {
17+
const opts = options || {};
18+
const maxBytes = typeof opts.maxBytes === 'number' ? opts.maxBytes : 10 * 1024;
19+
const headChars = typeof opts.headChars === 'number' ? opts.headChars : 512;
20+
const tailChars = typeof opts.tailChars === 'number' ? opts.tailChars : 512;
21+
22+
function toText(value) {
23+
if (value == null) {
24+
return '';
25+
}
26+
if (typeof value === 'string') {
27+
return value;
28+
}
29+
try {
30+
return JSON.stringify(value);
31+
} catch (e) {
32+
return String(value);
33+
}
34+
}
35+
36+
const text = toText(originalBody);
37+
let byteLength;
38+
if (typeof TextEncoder !== 'undefined') {
39+
byteLength = new TextEncoder().encode(text).length;
40+
} else {
41+
byteLength = text.length * 2;
42+
}
43+
44+
if (byteLength <= maxBytes) {
45+
return originalBody;
46+
}
47+
48+
return {
49+
truncated: true,
50+
size: byteLength + ' bytes',
51+
head: text.slice(0, headChars),
52+
tail: tailChars > 0 ? text.slice(-tailChars) : '',
53+
hint: '响应体过大,已截断显示'
54+
};
55+
};
56+
}
57+
358
// 域名白名单管理
459
let allowedDomains = new Set(['*']); // 默认允许所有域名,后续可以限制
560

@@ -184,13 +239,15 @@ async function handleCrossOriginRequest(request) {
184239
const responseBody = await response.text();
185240

186241
// 添加调试日志
242+
const safePreview = safeLogResponse(responseBody);
243+
187244
console.log('[Background] 响应详情:', {
188245
url,
189246
status: response.status,
190247
statusText: response.statusText,
191248
contentType: responseHeaders['content-type'] || 'unknown',
192249
bodyLength: responseBody.length,
193-
bodyPreview: responseBody.substring(0, 200)
250+
bodyPreview: safePreview
194251
});
195252

196253
// 同样发送到网页控制台
@@ -207,7 +264,7 @@ async function handleCrossOriginRequest(request) {
207264
status: response.status,
208265
contentType: responseHeaders['content-type'] || 'unknown',
209266
bodyLength: responseBody.length,
210-
bodyPreview: responseBody.substring(0, 200)
267+
bodyPreview: safePreview
211268
}
212269
})
213270
.catch(() => {});

content-script.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ const CrossRequest = {
111111
// 使用链式加载确保执行顺序,避免竞态条件
112112
const helpers = [
113113
'src/helpers/query-string.js',
114-
'src/helpers/body-parser.js'
114+
'src/helpers/body-parser.js',
115+
'src/helpers/logger.js'
115116
];
116117

117118
// 链式加载 helpers,然后加载 index.js

index.js

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,51 @@
6464
};
6565
}
6666

67+
// Fallback: safeLogResponse
68+
if (!helpers.safeLogResponse) {
69+
console.warn('[Index] safeLogResponse helper 未加载,使用内联 fallback');
70+
helpers.safeLogResponse = function (originalBody, options) {
71+
const opts = options || {};
72+
const maxBytes = typeof opts.maxBytes === 'number' ? opts.maxBytes : 10 * 1024;
73+
const headChars = typeof opts.headChars === 'number' ? opts.headChars : 512;
74+
const tailChars = typeof opts.tailChars === 'number' ? opts.tailChars : 512;
75+
76+
function toText(value) {
77+
if (value == null) {
78+
return '';
79+
}
80+
if (typeof value === 'string') {
81+
return value;
82+
}
83+
try {
84+
return JSON.stringify(value);
85+
} catch (e) {
86+
return String(value);
87+
}
88+
}
89+
90+
const text = toText(originalBody);
91+
let byteLength;
92+
if (typeof TextEncoder !== 'undefined') {
93+
byteLength = new TextEncoder().encode(text).length;
94+
} else {
95+
byteLength = text.length * 2;
96+
}
97+
98+
if (byteLength <= maxBytes) {
99+
return originalBody;
100+
}
101+
102+
return {
103+
truncated: true,
104+
size: byteLength + ' bytes',
105+
head: text.slice(0, headChars),
106+
tail: tailChars > 0 ? text.slice(-tailChars) : '',
107+
hint: '响应体过大,已截断显示'
108+
};
109+
};
110+
}
111+
67112
// 创建跨域请求的 API
68113
const CrossRequestAPI = {
69114
// 请求计数器
@@ -164,7 +209,7 @@
164209
debugLog('[Index] response.body 已是对象,直接使用:', {
165210
type: typeof response.body,
166211
isArray: Array.isArray(response.body),
167-
value: response.body
212+
value: helpers.safeLogResponse(response.body)
168213
});
169214
} else if (typeof response.body === 'string') {
170215
// 是字符串,需要解析
@@ -189,7 +234,7 @@
189234
parsedData = response.body;
190235
debugLog('[Index] response.body 是标量值,直接使用:', {
191236
type: typeof response.body,
192-
value: response.body
237+
value: helpers.safeLogResponse(response.body)
193238
});
194239
}
195240
} else if (response.body === undefined || response.body === null) {

manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"resources": [
3838
"src/helpers/query-string.js",
3939
"src/helpers/body-parser.js",
40+
"src/helpers/logger.js",
4041
"index.js"
4142
],
4243
"matches": ["<all_urls>"]

src/helpers/logger.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Logger Helper
3+
*
4+
* 提供安全的日志输出函数,用于在控制台中显示大响应体时进行截断。
5+
*/
6+
7+
(function (root) {
8+
'use strict';
9+
10+
var DEFAULT_MAX_BYTES = 10 * 1024; // 10KB
11+
var DEFAULT_HEAD_CHARS = 512;
12+
var DEFAULT_TAIL_CHARS = 512;
13+
14+
function formatBytes(bytes) {
15+
if (bytes < 1024) {
16+
return bytes + ' B';
17+
}
18+
if (bytes < 1024 * 1024) {
19+
return (bytes / 1024).toFixed(1) + ' KB';
20+
}
21+
if (bytes < 1024 * 1024 * 1024) {
22+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
23+
}
24+
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
25+
}
26+
27+
function toStringValue(value) {
28+
if (value == null) {
29+
return '';
30+
}
31+
32+
if (typeof value === 'string') {
33+
return value;
34+
}
35+
36+
try {
37+
return JSON.stringify(value);
38+
} catch (e) {
39+
try {
40+
return String(value);
41+
} catch (stringError) {
42+
return '[无法序列化的响应体]';
43+
}
44+
}
45+
}
46+
47+
function countBytes(text) {
48+
if (typeof TextEncoder !== 'undefined') {
49+
return new TextEncoder().encode(text).length;
50+
}
51+
// 退化处理:按字符长度估算
52+
return text.length * 2;
53+
}
54+
55+
/**
56+
* 构造安全的日志输出
57+
* @param {*} originalBody 原始响应体
58+
* @param {Object} [options]
59+
* @param {number} [options.maxBytes] 最大允许的字节数(默认 10KB)
60+
* @param {number} [options.headChars] 截断时保留的头部字符数
61+
* @param {number} [options.tailChars] 截断时保留的尾部字符数
62+
* @returns {*} 原始响应体或截断后的摘要对象
63+
*/
64+
function safeLogResponse(originalBody, options) {
65+
var opts = options || {};
66+
var maxBytes = typeof opts.maxBytes === 'number' ? opts.maxBytes : DEFAULT_MAX_BYTES;
67+
var headChars = typeof opts.headChars === 'number' ? opts.headChars : DEFAULT_HEAD_CHARS;
68+
var tailChars = typeof opts.tailChars === 'number' ? opts.tailChars : DEFAULT_TAIL_CHARS;
69+
70+
var textValue = toStringValue(originalBody);
71+
var bytes = countBytes(textValue);
72+
73+
if (bytes <= maxBytes) {
74+
return originalBody;
75+
}
76+
77+
var previewHead = textValue.slice(0, headChars);
78+
var previewTail = tailChars > 0 ? textValue.slice(-tailChars) : '';
79+
80+
return {
81+
truncated: true,
82+
size: formatBytes(bytes),
83+
head: previewHead,
84+
tail: previewTail,
85+
hint: '响应体过大,已截断显示(查看 head/tail 字段获取片段)'
86+
};
87+
}
88+
89+
root.CrossRequestHelpers = root.CrossRequestHelpers || {};
90+
root.CrossRequestHelpers.safeLogResponse = safeLogResponse;
91+
92+
if (typeof module !== 'undefined' && module.exports) {
93+
module.exports = {
94+
safeLogResponse: safeLogResponse,
95+
__private: {
96+
formatBytes: formatBytes,
97+
toStringValue: toStringValue
98+
}
99+
};
100+
}
101+
})(typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : globalThis));

tests/helpers.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// Import real helpers from production code
1414
const { bodyToString } = require('../src/helpers/body-parser.js');
1515
const { buildQueryString } = require('../src/helpers/query-string.js');
16+
const { safeLogResponse } = require('../src/helpers/logger.js');
1617

1718
describe('bodyToString helper', () => {
1819

@@ -464,3 +465,28 @@ describe('GET request parameter handling', () => {
464465
});
465466
});
466467

468+
describe('safeLogResponse helper', () => {
469+
test('should return original body when below threshold', () => {
470+
const small = 'hello world';
471+
const result = safeLogResponse(small, { maxBytes: 100 });
472+
expect(result).toBe(small);
473+
});
474+
475+
test('should truncate large string responses', () => {
476+
const large = 'a'.repeat(20 * 1024); // 20KB
477+
const result = safeLogResponse(large, { maxBytes: 10 * 1024, headChars: 100, tailChars: 50 });
478+
expect(result).toHaveProperty('truncated', true);
479+
expect(result).toHaveProperty('size');
480+
expect(result.head.length).toBe(100);
481+
expect(result.tail.length).toBe(50);
482+
});
483+
484+
test('should stringify objects and truncate', () => {
485+
const payload = { data: 'x'.repeat(15 * 1024) };
486+
const result = safeLogResponse(payload, { maxBytes: 10 * 1024, headChars: 50, tailChars: 50 });
487+
expect(result.truncated).toBe(true);
488+
expect(typeof result.head).toBe('string');
489+
expect(typeof result.tail).toBe('string');
490+
expect(result.hint).toContain('截断');
491+
});
492+
});

0 commit comments

Comments
 (0)