fix: 旧版 Node 与 iOS 环境下请求降级至 node-fetch v3 修复响应头丢失导致个别源弹幕获取失败 (#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 及静态判定缓存动机,去除冗余推导表述。
This commit is contained in:
1 parent
17b3f4d723
commit
89fd166424
2 files changed
+64
-14
No files matched your search
@@ -23,6 +23,11 @@ import { startFavoriteScheduler, stopFavoriteScheduler } from './utils/favorite-
|
||||
// 导入 ES module 兼容层(始终加载,但内部会根据需要启用)
|
||||
import './esm-shim.cjs';
|
||||
|
||||
// 预加载 node-fetch v3:仅在 Node < 20.19.0 等需兼容层的环境实际加载,其他环境为无操作;必须在首个请求前完成,否则 esm-shim 的 require 代理会拒绝同步取用
|
||||
if (typeof global.loadNodeFetch === 'function') {
|
||||
await global.loadNodeFetch();
|
||||
}
|
||||
|
||||
// 构建 CommonJS 环境下才有的全局变量
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { globals } from '../configs/globals.js';
|
||||
import { log } from './log-util.js'
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
import https from 'node:https';
|
||||
import http from 'node:http';
|
||||
|
||||
// 跨异步生命周期链路的日志上下文追踪器
|
||||
export const sourceLogContext = new AsyncLocalStorage();
|
||||
@@ -59,6 +61,31 @@ function linkSignal(externalSignal, internalController) {
|
||||
};
|
||||
}
|
||||
|
||||
// 旧版 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。判定仅依赖静态环境、进程内恒定,故模块加载时算一次并缓存。
|
||||
function detectNodeFetchDowngrade() {
|
||||
if (typeof WebAssembly === 'undefined') return true;
|
||||
const [major, minor] = process.versions.node.split('.').map(Number);
|
||||
return major < 20 || (major === 20 && minor < 19);
|
||||
}
|
||||
|
||||
const USE_NODE_FETCH = detectNodeFetchDowngrade();
|
||||
if (USE_NODE_FETCH) {
|
||||
// 模块载入时 logLevel 尚未初始化,用 console.log 保证启动提示必现
|
||||
console.log("[system] [http] 检测到旧版Node/iOS环境,已全局切换至 node-fetch v3 作为请求实现");
|
||||
}
|
||||
|
||||
// 降级分支共享 keep-alive Agent,复用 TCP/TLS 连接以与原生 undici 连接池达到实际等价(消除重复握手开销);按协议区分 https/http
|
||||
const nodeFetchHttpsAgent = USE_NODE_FETCH ? new https.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null;
|
||||
const nodeFetchHttpAgent = USE_NODE_FETCH ? new http.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null;
|
||||
function nodeFetchAgent(parsedUrl) {
|
||||
const protocol = parsedUrl instanceof URL ? parsedUrl.protocol : new URL(parsedUrl).protocol;
|
||||
return protocol === 'https:' ? nodeFetchHttpsAgent : nodeFetchHttpAgent;
|
||||
}
|
||||
|
||||
function shouldUseNodeFetch() {
|
||||
return USE_NODE_FETCH;
|
||||
}
|
||||
|
||||
export async function httpGet(url, options = {}) {
|
||||
// 单次搜索请求内 HTTP 响应复用: 若当前请求上下文已激活复用缓存且本 URL 已缓存, 直接返回克隆结果, 跳过重复网络请求
|
||||
const requestHttpCache = httpCacheContext.getStore();
|
||||
@@ -103,17 +130,17 @@ export async function httpGet(url, options = {}) {
|
||||
const cleanupSignal = linkSignal(options.signal, controller);
|
||||
|
||||
try {
|
||||
// 兼容iOS巨魔环境:使用node-fetch替代内置fetch
|
||||
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
|
||||
let response;
|
||||
if (typeof WebAssembly === 'undefined') {
|
||||
log("info", "[system] [http] iOS环境降级使用node-fetch");
|
||||
if (shouldUseNodeFetch()) {
|
||||
const fetch = (await import('node-fetch')).default;
|
||||
response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...options.headers,
|
||||
},
|
||||
signal: controller.signal
|
||||
signal: controller.signal,
|
||||
agent: nodeFetchAgent
|
||||
});
|
||||
} else {
|
||||
// 现代浏览器环境
|
||||
@@ -328,12 +355,11 @@ export async function httpPost(url, body, options = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
// 兼容iOS巨魔环境:使用node-fetch替代内置fetch
|
||||
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
|
||||
let response;
|
||||
if (typeof WebAssembly === 'undefined') {
|
||||
log("info", "[system] [http] iOS环境降级使用node-fetch");
|
||||
if (shouldUseNodeFetch()) {
|
||||
const fetch = (await import('node-fetch')).default;
|
||||
response = await fetch(url, fetchOptions);
|
||||
response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent });
|
||||
} else {
|
||||
// 现代浏览器环境
|
||||
response = await fetch(url, fetchOptions);
|
||||
@@ -452,7 +478,14 @@ async function httpRequestMethod(method, url, body, options = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, fetchOptions);
|
||||
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
|
||||
let response;
|
||||
if (shouldUseNodeFetch()) {
|
||||
const fetch = (await import('node-fetch')).default;
|
||||
response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent });
|
||||
} else {
|
||||
response = await fetch(url, fetchOptions);
|
||||
}
|
||||
const textData = await response.text();
|
||||
|
||||
if (!response.ok && !validStatusCodes.includes(response.status)) {
|
||||
@@ -712,11 +745,23 @@ export async function httpGetWithStreamCheck(url, options = {}, checkCallback) {
|
||||
const currentSource = sourceLogContext.getStore() || "system";
|
||||
log("info", `[${currentSource}] [流式请求] HTTP GET: ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
signal: controller.signal
|
||||
});
|
||||
// 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch
|
||||
let response;
|
||||
if (shouldUseNodeFetch()) {
|
||||
const fetch = (await import('node-fetch')).default;
|
||||
response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
signal: controller.signal,
|
||||
agent: nodeFetchAgent
|
||||
});
|
||||
} else {
|
||||
response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
signal: controller.signal
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
Reference in new issue
Block a user