-
Notifications
You must be signed in to change notification settings - Fork 307
responseHeaders: TM兼容: \r\n
#1085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
另外感觉TM返回的headers的是不是排序了?(这个我觉得没必要也跟随了) |
有注意到。但感觉 TM 不是故意用了 sort.
对。如果加 sort 会拖慢 |
| this.responseURL = res.url ?? this.url; | ||
| this._responseHeaders = { | ||
| getAllResponseHeaders(): string { | ||
| let ret: string | undefined = this.cache[""]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个cache的key怎么是 空文本
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
有 key 的是单一 header (getResponseHeader). 空 key 是 all (getAllResponseHeaders)
只是用了同一个 cache 来处理
当然不好看的话也可以拆开做多一个 variable
9aed140 to
47e9359
Compare
Co-Authored-By: wangyizhi <yz@ggnb.top>
47e9359 to
5296f49
Compare
src/pkg/utils/utils.ts
Outdated
| export const nativeResponseHeadersTreatment = (headersString: string) => { | ||
| if (!headersString) return ""; | ||
| const len = headersString.length; | ||
| let out = ""; | ||
| let start = len; // start position = nil | ||
| let separator = ""; | ||
| for (let i = 0; i <= len; i++) { | ||
| const char = headersString.charCodeAt(i) || 10; | ||
| if (char === 10 || char === 13) { | ||
| if (i > start) { | ||
| const seg = headersString.substring(start, i); // "key: value" | ||
| const j = seg.indexOf(":"); | ||
| if (j > 0) { | ||
| let k = j + 1; | ||
| if (seg.charCodeAt(k) === 32) k++; | ||
| if (k < seg.length) { | ||
| const headerName = seg.substring(0, j); // "key" | ||
| const headerValue = seg.substring(k); // "value" | ||
| out += `${separator}${headerName}:${headerValue}`; | ||
| separator = "\r\n"; | ||
| } | ||
| } | ||
| } | ||
| start = len; // start position = nil | ||
| } else { | ||
| if (start === len) { | ||
| start = i; // set start position | ||
| } | ||
| } | ||
| } | ||
| return out; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
真心希望使用可读性更好的方式去实现,虽然大概明白是个什么逻辑,也处理了很多异常的情况,当然不是说不好,我觉得很厉害,但是阅读起来好困难
我们拿到的header应该是浏览器已经处理过一遍的header,而且http协议应该也不允许那种异常的header,按照http协议来说,结尾一定是 \r\n
https://developer.mozilla.org/zh-CN/docs/Glossary/HTTP_header
看你吧
// TM Xhr Header 兼容处理,原生xhr \r\n 在尾,但TM的GMXhr没有;同时除去冒号后面的空白
export const nativeResponseHeadersTreatment = (headersString: string) => {
if (!headersString) return "";
let out = "";
let separator = "";
headersString.split(/\r?\n/).forEach((line) => {
const j = line.indexOf(":");
if (j > 0) {
const headerName = line.substring(0, j); // "key"
let headerValue = line.substring(j + 1); // "value"
let k = 0;
// 删除开头空白
for (; k < headerValue.length; k += 1) {
if (headerValue[k] !== " ") {
break;
}
}
headerValue = headerValue.substring(k);
out += `${separator}${headerName}:${headerValue}`;
separator = "\r\n";
}
});
return out;
};There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
headersString.split(/\r?\n/)
不想用 regex. 效能考虑
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
做了个基准测试,好像性能并不好。。。。。:
✓ src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试 8784ms
name hz min max mean p75 p99 p995 p999 rme samples
· 简单响应头 2,731,094.40 0.0002 5.7630 0.0004 0.0003 0.0004 0.0006 0.0010 ±4.16% 1365548
· 包含多余空格的响应头 1,931,481.53 0.0004 0.4018 0.0005 0.0005 0.0006 0.0011 0.0015 ±0.50% 965741
· 复杂真实场景响应头 468,745.09 0.0020 0.2766 0.0021 0.0021 0.0025 0.0028 0.0062 ±0.26% 234373
· 混合分隔符响应头 1,495,232.50 0.0005 0.9980 0.0007 0.0006 0.0015 0.0015 0.0016 ±0.84% 747617
· 空字符串 24,717,513.26 0.0000 0.9977 0.0000 0.0000 0.0000 0.0000 0.0001 ±0.51% 12358757
· 长响应头 (100个header) 54,875.94 0.0171 0.5177 0.0182 0.0175 0.0199 0.0340 0.4157 ±1.03% 27438
BENCH Summary
空字符串 - src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试
9.05x faster than 简单响应头
12.80x faster than 包含多余空格的响应头
16.53x faster than 混合分隔符响应头
52.73x faster than 复杂真实场景响应头
450.43x faster than 长响应头 (100个header)
✓ src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试 8989ms
name hz min max mean p75 p99 p995 p999 rme samples
· 简单响应头 3,731,787.16 0.0001 0.6287 0.0003 0.0003 0.0004 0.0007 0.0010 ±1.05% 1865894
· 包含多余空格的响应头 2,648,493.16 0.0002 8.9453 0.0004 0.0003 0.0004 0.0006 0.0010 ±4.20% 1324247
· 复杂真实场景响应头 793,283.50 0.0011 0.5739 0.0013 0.0012 0.0017 0.0019 0.0034 ±0.64% 396642
· 混合分隔符响应头 2,360,763.12 0.0003 3.7784 0.0004 0.0004 0.0005 0.0010 0.0011 ±1.77% 1180382
· 空字符串 23,601,426.73 0.0000 0.2814 0.0000 0.0000 0.0001 0.0001 0.0001 ±0.30% 11800714
· 长响应头 (100个header) 102,675.98 0.0087 0.8815 0.0097 0.0090 0.0103 0.0197 0.3522 ±1.40% 51338
BENCH Summary
空字符串 - src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试
6.32x faster than 简单响应头
8.91x faster than 包含多余空格的响应头
10.00x faster than 混合分隔符响应头
29.75x faster than 复杂真实场景响应头
229.86x faster than 长响应头 (100个header)
import { bench, describe } from "vitest";
import { nativeResponseHeadersTreatment } from "./utils";
describe("nativeResponseHeadersTreatment 基准测试", () => {
// 测试用例1: 简单的响应头
const simpleHeaders = "content-type: application/json\r\nserver: nginx";
// 测试用例2: 包含多余空格的响应头
const headersWithSpaces = "content-type: application/json\r\nserver: nginx\r\ncache-control: no-cache";
// 测试用例3: 复杂的响应头(真实场景)
const complexHeaders = `content-type: text/html; charset=utf-8\r\n
server: Apache/2.4.41 (Ubuntu)\r\n
set-cookie: sessionid=abc123; Path=/; HttpOnly\r\n
cache-control: no-store, no-cache, must-revalidate\r\n
expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n
pragma: no-cache\r\n
x-frame-options: SAMEORIGIN\r\n
x-content-type-options: nosniff\r\n
content-security-policy: default-src 'self'\r\n
access-control-allow-origin: *`;
// 测试用例4: 包含多种分隔符的响应头(混合 \r\n 和 \n)
const mixedHeaders = "content-type: application/json\nserver: nginx\r\ncache-control: no-cache\ncontent-length: 1234";
// 测试用例5: 空字符串
const emptyHeaders = "";
// 测试用例6: 很长的响应头(性能压力测试)
const longHeaders = Array(100)
.fill(0)
.map((_, i) => `x-custom-header-${i}: value-${i}`)
.join("\r\n");
bench("简单响应头", () => {
nativeResponseHeadersTreatment(simpleHeaders);
});
bench("包含多余空格的响应头", () => {
nativeResponseHeadersTreatment(headersWithSpaces);
});
bench("复杂真实场景响应头", () => {
nativeResponseHeadersTreatment(complexHeaders);
});
bench("混合分隔符响应头", () => {
nativeResponseHeadersTreatment(mixedHeaders);
});
bench("空字符串", () => {
nativeResponseHeadersTreatment(emptyHeaders);
});
bench("长响应头 (100个header)", () => {
nativeResponseHeadersTreatment(longHeaders);
});
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我再看看
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
呀。測試方法有問題
對同一個 string 做 regex, V8內部會cache 起來
bench(...) 會跑同一個N次
第一次是真時間,第2~N次都是假的
let atomicId = 0;
bench("简单响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)", () => {
nativeResponseHeadersTreatment(`${++atomicId}${longHeaders}`);
});我估計上面是 regex 下面是我的版本
你會看到 上面的 max 很大
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
你的写法和思路更像是在弄 C/C++ ,不过说实话,哪怕是性能更好,我也宁愿牺牲掉这么一点微不足道的性能换取可读性,可读性差加大了其他人的阅读门槛,类似的问题我提了好多次了,后续我可能要重新思考这些为了性能妥协的内容了 😣
当然也不是说性能问题不重要,只是不用在这种很细小的问题上去钻,一般都是在量级上来了后才会体现出问题来,而且这个量级几乎不会达到。
就我来说,一般只会考虑实际情况可能会达到一定量级的地方,才会特意的去做性能优化
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { bench, describe } from "vitest";
// TM Xhr Header 兼容处理,原生xhr \r\n 在尾,但TM的GMXhr没有;同时除去冒号后面的空白
export const nativeResponseHeadersTreatment1 = (hs: string) => {
let start = 0;
let out = "";
const len = hs.length;
let separator = "";
while (start < len) {
const i = hs.indexOf(":", start); // 冒号的位置
let j = hs.indexOf("\n", start); // 换行符的位置
if (j < 0) j = len; // 尾行
if (i < j) {
const key = hs.substring(start, i).trim(); // i 为 -1 的话 key 也会是空值
if (key) {
out += `${separator}${key}:${hs.substring(i + 1, j).trim()}`;
separator = "\r\n";
}
}
start = j + 1; // 移到下一行
}
return out;
};
export const nativeResponseHeadersTreatment2 = (headersString: string) => {
if (!headersString) return "";
let out = "";
let separator = "";
headersString.split(/\r?\n/).forEach((line) => {
const j = line.indexOf(":");
if (j > 0) {
const headerName = line.substring(0, j); // "key"
let headerValue = line.substring(j + 1).trim(); // "value"
let k = 0;
// 删除开头空白
for (; k < headerValue.length; k += 1) {
if (headerValue[k] !== " ") {
break;
}
}
headerValue = headerValue.substring(k);
out += `${separator}${headerName}:${headerValue}`;
separator = "\r\n";
}
});
return out;
};
describe("nativeResponseHeadersTreatment 基准测试", () => {
// 测试用例1: 简单的响应头
const simpleHeaders = "content-type: application/json\r\nserver: nginx";
// 测试用例2: 包含多余空格的响应头
const headersWithSpaces = "content-type: application/json\r\nserver: nginx\r\ncache-control: no-cache";
// 测试用例3: 复杂的响应头(真实场景)
const complexHeaders = `content-type: text/html; charset=utf-8\r\n
server: Apache/2.4.41 (Ubuntu)\r\n
set-cookie: sessionid=abc123; Path=/; HttpOnly\r\n
cache-control: no-store, no-cache, must-revalidate\r\n
expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n
pragma: no-cache\r\n
x-frame-options: SAMEORIGIN\r\n
x-content-type-options: nosniff\r\n
content-security-policy: default-src 'self'\r\n
access-control-allow-origin: *`;
// 测试用例4: 包含多种分隔符的响应头(混合 \r\n 和 \n)
const mixedHeaders = "content-type: application/json\nserver: nginx\r\ncache-control: no-cache\ncontent-length: 1234";
// 测试用例5: 空字符串
// const emptyHeaders = "";
// 测试用例6: 很长的响应头(性能压力测试)
const longHeaders = Array(100)
.fill(0)
.map((_, i) => `x-custom-header-${i}: value-${i}`)
.join("\r\n");
let atomicId = 10_000_000;
bench("简单响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${longHeaders}`);
});
bench("简单响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${longHeaders}`);
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
如果不处理 \r 的话,我也修改了并通过了单元测试,某些情况会好一点点,差距极小,但是这点差距有什么意义呢,你为什么如此的排斥语法糖,JS本来就是一个解释型的语言,他不像编译型的语言,一些操作可以直接访问内存会更快,一些语法糖V8有内部的优化,你以为的一些优化可能并没有效果,反而会加重解释器的负担
我可能说服不了你,但是我不想再因为这些舍弃可读性和架构整洁了,除了你、我可能还会有其他人来阅读,水平不够或者不了解的人只会两眼一黑,为了读懂这代码所消耗的能量已经够SC所有用户跑上几十年了:
✓ src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试 9305ms
name hz min max mean p75 p99 p995 p999 rme samples
· 简单响应头-1 3,101,845.11 0.0001 8.3151 0.0003 0.0002 0.0005 0.0006 0.0050 ±5.48% 1550923
· 包含多余空格的响应头-1 3,232,974.52 0.0002 1.2363 0.0003 0.0003 0.0003 0.0007 0.0010 ±2.81% 1616488
· 复杂真实场景响应头-1 865,962.22 0.0008 2.8180 0.0012 0.0010 0.0016 0.0024 0.0175 ±3.14% 433564
· 混合分隔符响应头-1 2,533,276.00 0.0002 3.1258 0.0004 0.0003 0.0004 0.0008 0.0012 ±3.56% 1266688
· 长响应头 (100个header)-1 137,587.81 0.0064 0.5487 0.0073 0.0068 0.0083 0.0169 0.2648 ±1.18% 68794
· 简单响应头-2 2,703,374.50 0.0002 2.3180 0.0004 0.0003 0.0004 0.0008 0.0012 ±3.77% 1351688
· 包含多余空格的响应头-2 2,371,600.25 0.0003 1.9080 0.0004 0.0004 0.0005 0.0008 0.0011 ±2.96% 1185801
· 复杂真实场景响应头-2 914,793.00 0.0008 0.8549 0.0011 0.0010 0.0014 0.0016 0.0045 ±2.37% 457397
· 混合分隔符响应头-2 2,450,759.48 0.0003 0.6513 0.0004 0.0004 0.0005 0.0005 0.0011 ±0.30% 1225380
· 长响应头 (100个header)-2 140,328.80 0.0062 0.6886 0.0071 0.0066 0.0083 0.0157 0.2791 ±1.28% 70165
BENCH Summary
包含多余空格的响应头-1 - src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试
1.04x faster than 简单响应头-1
1.20x faster than 简单响应头-2
1.28x faster than 混合分隔符响应头-1
1.32x faster than 混合分隔符响应头-2
1.36x faster than 包含多余空格的响应头-2
3.53x faster than 复杂真实场景响应头-2
3.73x faster than 复杂真实场景响应头-1
23.04x faster than 长响应头 (100个header)-2
23.50x faster than 长响应头 (100个header)-1
import { bench, describe } from "vitest";
// TM Xhr Header 兼容处理,原生xhr \r\n 在尾,但TM的GMXhr没有;同时除去冒号后面的空白
export const nativeResponseHeadersTreatment1 = (hs: string) => {
let start = 0;
let out = "";
const len = hs.length;
let separator = "";
while (start < len) {
const i = hs.indexOf(":", start); // 冒号的位置
let j = hs.indexOf("\n", start); // 换行符的位置
if (j < 0) j = len; // 尾行
if (i < j) {
const key = hs.substring(start, i).trim(); // i 为 -1 的话 key 也会是空值
if (key) {
out += `${separator}${key}:${hs.substring(i + 1, j).trim()}`;
separator = "\r\n";
}
}
start = j + 1; // 移到下一行
}
return out;
};
export const nativeResponseHeadersTreatment2 = (headersString: string) => {
if (!headersString) return "";
let out = "";
headersString.split("\n").forEach((line) => {
const j = line.indexOf(":");
if (j > 0) {
const headerName = line.substring(0, j); // "key"
const headerValue = line.substring(j + 1).trim(); // "value"
out += `${headerName}:${headerValue}\r\n`;
}
});
return out.substring(0, out.length - 2); // 去掉最后的 \r\n
};
describe("nativeResponseHeadersTreatment 基准测试", () => {
// 测试用例1: 简单的响应头
const simpleHeaders = "content-type: application/json\r\nserver: nginx";
// 测试用例2: 包含多余空格的响应头
const headersWithSpaces = "content-type: application/json\r\nserver: nginx\r\ncache-control: no-cache";
// 测试用例3: 复杂的响应头(真实场景)
const complexHeaders = `content-type: text/html; charset=utf-8\r\n
server: Apache/2.4.41 (Ubuntu)\r\n
set-cookie: sessionid=abc123; Path=/; HttpOnly\r\n
cache-control: no-store, no-cache, must-revalidate\r\n
expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n
pragma: no-cache\r\n
x-frame-options: SAMEORIGIN\r\n
x-content-type-options: nosniff\r\n
content-security-policy: default-src 'self'\r\n
access-control-allow-origin: *`;
// 测试用例4: 包含多种分隔符的响应头(混合 \r\n 和 \n)
const mixedHeaders = "content-type: application/json\nserver: nginx\r\ncache-control: no-cache\ncontent-length: 1234";
// 测试用例5: 空字符串
// const emptyHeaders = "";
// 测试用例6: 很长的响应头(性能压力测试)
const longHeaders = Array(100)
.fill(0)
.map((_, i) => `x-custom-header-${i}: value-${i}`)
.join("\r\n");
let atomicId = 10_000_000;
bench("简单响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${longHeaders}`);
});
bench("简单响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${longHeaders}`);
});
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
修改后的 nativeResponseHeadersTreatment1 已经是很好读的吧
就是简单的一行一行处理
你要用 nativeResponseHeadersTreatment2 我也没意见
你觉得 headersString.split("\n").forEach((line) => { ... } 是好一点阅读也可以呀
我没所谓
操作多,日后不预期大改动,PR的写法都尽量平衡阅读和效能
这个东西写了之后就永远不用改动。
也不是几页纸的长度
这20行写的是很一般的程序
里面又没有什么复杂 algorithm
我觉得是观点角度问题啦。我认为 7a2dbe6 这个修改已经是相关简化
不过你不欣赏对字串用这种看似C代码的写法,也是一种看法
像一个没写开 React 的人,看到 React 都会完全看不懂一样
每个人的看法不一样吧
在我看起来,现在只用了 indexOf, while 等东西,是一个简单合理的写法
你不喜欢也行呀,改成你的 split("\n") 吧
当然这不是重大的效能影响
但 12 行 跟 20 行 的代码,我看起来不懂的人还是不懂,懂的人还是懂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
相比之前确实好一些,但是依旧没有 nativeResponseHeadersTreatment2 的直接,这里的主要目的还是希望你不要那么排斥语法糖,性能并没有那么差
那我修改为 nativeResponseHeadersTreatment2 了
| } | ||
| return out; | ||
| }); | ||
| return out.substring(0, out.length - 2); // 去掉最后的 \r\n |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
注:这段可以用 ${separator}${headerName}:${headerValue} 这样来避免最后呼叫 substring
不过作者认为这些是过度优化那就算了
* responseHeaders: `TM兼容: 使用 \r\n 及不包含空白` Co-Authored-By: wangyizhi <yz@ggnb.top> * 中文 * 代码调整 * added streamReader error * 代码调整 * 代码调整 * 加注释 * nativeResponseHeadersTreatment -> normalizeResponseHeaders * Update bg_gm_xhr.ts * update * 代码调整 * 代码调整 * 单元测试 * 修改 normalizeResponseHeaders 实现 --------- Co-Authored-By: wangyizhi <yz@ggnb.top>
很多腳本也有用
response.responseHeaders.split("\r\n")https://github.com/search?q=+responseHeaders.split%28%22%5Cr%5Cn%22%29&type=code