Skip to content

Commit 89fd166

Browse files
authored
fix: 旧版 Node 与 iOS 环境下请求降级至 node-fetch v3 修复响应头丢失导致个别源弹幕获取失败 (huangxd-#431)
* fix: 旧版 Node 下改用 node-fetch v3 以修复 Set-Cookie 被丢弃 Node 18 自带的 undici(fetch 实验特性)在解析响应头时会丢弃 Set-Cookie,导致 youku 等依赖该响应头做令牌握手的请求在 Node < 20.19.0 下失败(FAIL_SYS_TOKEN_EMPTY、segmentList is not iterable)。 将请求层(http-util.js)原仅针对 iOS 巨魔环境的 node-fetch v3 降级条件扩展到「Node < 20.19.0」,与 esm-shim 兼容性边界 20.19.0 对齐;并在 server.js 启动处预加载 node-fetch v3,确保 esm-shim 的 require 代理可被同步取用。httpGet / httpPost / httpRequestMethod / httpGetWithStreamCheck 四处 fetch 调用点统一经 shouldUseNodeFetch() 判定,Node >= 20.19.0 仍走原生 fetch,行为与修复前一致。 * 优化: 旧版Node/iOS降级路径启用 keep-alive 连接复用并收敛降级日志为单次 旧版 Node / iOS 环境下统一降级到 node-fetch v3 后,node-fetch 默认走 keepAlive:false 的全局 Agent,并发场景(如跨季并行 mapping)下比原生 undici 连接池更重。本提交在降级分支接入共享 keep-alive Agent(按协议区分 https/http),复用 TCP/TLS 连接,与原生 undici 连接池达到实际等价。 同时将原本 4 个 fetch 调用点「每次请求都输出」的降级日志收敛为模块加载时计算一次、运行时单次启动日志,避免降级环境下日志刷屏。降级判定仅依赖静态环境、进程内恒定,故可安全缓存。 * 文档: 精简 http-util 降级判定的函数注释 将 detectNodeFetchDowngrade 上方注释压缩为单行,保留降级边界(Node<20.19.0 与 esm-shim 一致)、根因(旧 undici 丢弃 Set-Cookie)、iOS 无 WebAssembly 及静态判定缓存动机,去除冗余推导表述。
1 parent 17b3f4d commit 89fd166

2 files changed

Lines changed: 64 additions & 14 deletions

File tree

danmu_api/server.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import { startFavoriteScheduler, stopFavoriteScheduler } from './utils/favorite-
2323
// 导入 ES module 兼容层(始终加载,但内部会根据需要启用)
2424
import './esm-shim.cjs';
2525

26+
// 预加载 node-fetch v3:仅在 Node < 20.19.0 等需兼容层的环境实际加载,其他环境为无操作;必须在首个请求前完成,否则 esm-shim 的 require 代理会拒绝同步取用
27+
if (typeof global.loadNodeFetch === 'function') {
28+
await global.loadNodeFetch();
29+
}
30+
2631
// 构建 CommonJS 环境下才有的全局变量
2732
const __filename = fileURLToPath(import.meta.url);
2833
const __dirname = path.dirname(__filename);

danmu_api/utils/http-util.js

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { globals } from '../configs/globals.js';
22
import { log } from './log-util.js'
33
import { AsyncLocalStorage } from 'node:async_hooks';
4+
import https from 'node:https';
5+
import http from 'node:http';
46

57
// 跨异步生命周期链路的日志上下文追踪器
68
export const sourceLogContext = new AsyncLocalStorage();
@@ -59,6 +61,31 @@ function linkSignal(externalSignal, internalController) {
5961
};
6062
}
6163

64+
// 旧版 Node(<20.19.0,自带 undici 解析响应头时丢弃 Set-Cookie)与 iOS 巨魔(无 WebAssembly、无原生 fetch)改用 node-fetch v3(其 Headers 正常暴露 Set-Cookie);降级边界与 esm-shim 的 20.19.0 一致,Node >= 20.19.0 仍用原生 fetch。判定仅依赖静态环境、进程内恒定,故模块加载时算一次并缓存。
65+
function detectNodeFetchDowngrade() {
66+
if (typeof WebAssembly === 'undefined') return true;
67+
const [major, minor] = process.versions.node.split('.').map(Number);
68+
return major < 20 || (major === 20 && minor < 19);
69+
}
70+
71+
const USE_NODE_FETCH = detectNodeFetchDowngrade();
72+
if (USE_NODE_FETCH) {
73+
// 模块载入时 logLevel 尚未初始化,用 console.log 保证启动提示必现
74+
console.log("[system] [http] 检测到旧版Node/iOS环境,已全局切换至 node-fetch v3 作为请求实现");
75+
}
76+
77+
// 降级分支共享 keep-alive Agent,复用 TCP/TLS 连接以与原生 undici 连接池达到实际等价(消除重复握手开销);按协议区分 https/http
78+
const nodeFetchHttpsAgent = USE_NODE_FETCH ? new https.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null;
79+
const nodeFetchHttpAgent = USE_NODE_FETCH ? new http.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null;
80+
function nodeFetchAgent(parsedUrl) {
81+
const protocol = parsedUrl instanceof URL ? parsedUrl.protocol : new URL(parsedUrl).protocol;
82+
return protocol === 'https:' ? nodeFetchHttpsAgent : nodeFetchHttpAgent;
83+
}
84+
85+
function shouldUseNodeFetch() {
86+
return USE_NODE_FETCH;
87+
}
88+
6289
export async function httpGet(url, options = {}) {
6390
// 单次搜索请求内 HTTP 响应复用: 若当前请求上下文已激活复用缓存且本 URL 已缓存, 直接返回克隆结果, 跳过重复网络请求
6491
const requestHttpCache = httpCacheContext.getStore();
@@ -103,17 +130,17 @@ export async function httpGet(url, options = {}) {
103130
const cleanupSignal = linkSignal(options.signal, controller);
104131

105132
try {
106-
// 兼容iOS巨魔环境:使用node-fetch替代内置fetch
133+
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
107134
let response;
108-
if (typeof WebAssembly === 'undefined') {
109-
log("info", "[system] [http] iOS环境降级使用node-fetch");
135+
if (shouldUseNodeFetch()) {
110136
const fetch = (await import('node-fetch')).default;
111137
response = await fetch(url, {
112138
method: 'GET',
113139
headers: {
114140
...options.headers,
115141
},
116-
signal: controller.signal
142+
signal: controller.signal,
143+
agent: nodeFetchAgent
117144
});
118145
} else {
119146
// 现代浏览器环境
@@ -328,12 +355,11 @@ export async function httpPost(url, body, options = {}) {
328355
}
329356

330357
try {
331-
// 兼容iOS巨魔环境:使用node-fetch替代内置fetch
358+
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
332359
let response;
333-
if (typeof WebAssembly === 'undefined') {
334-
log("info", "[system] [http] iOS环境降级使用node-fetch");
360+
if (shouldUseNodeFetch()) {
335361
const fetch = (await import('node-fetch')).default;
336-
response = await fetch(url, fetchOptions);
362+
response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent });
337363
} else {
338364
// 现代浏览器环境
339365
response = await fetch(url, fetchOptions);
@@ -452,7 +478,14 @@ async function httpRequestMethod(method, url, body, options = {}) {
452478
}
453479

454480
try {
455-
const response = await fetch(url, fetchOptions);
481+
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
482+
let response;
483+
if (shouldUseNodeFetch()) {
484+
const fetch = (await import('node-fetch')).default;
485+
response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent });
486+
} else {
487+
response = await fetch(url, fetchOptions);
488+
}
456489
const textData = await response.text();
457490

458491
if (!response.ok && !validStatusCodes.includes(response.status)) {
@@ -712,11 +745,23 @@ export async function httpGetWithStreamCheck(url, options = {}, checkCallback) {
712745
const currentSource = sourceLogContext.getStore() || "system";
713746
log("info", `[${currentSource}] [流式请求] HTTP GET: ${url}`);
714747

715-
const response = await fetch(url, {
716-
method: 'GET',
717-
headers: headers,
718-
signal: controller.signal
719-
});
748+
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
749+
let response;
750+
if (shouldUseNodeFetch()) {
751+
const fetch = (await import('node-fetch')).default;
752+
response = await fetch(url, {
753+
method: 'GET',
754+
headers: headers,
755+
signal: controller.signal,
756+
agent: nodeFetchAgent
757+
});
758+
} else {
759+
response = await fetch(url, {
760+
method: 'GET',
761+
headers: headers,
762+
signal: controller.signal
763+
});
764+
}
720765

721766
if (!response.ok) {
722767
throw new Error(`HTTP error! status: ${response.status}`);

0 commit comments

Comments
 (0)