fix: 修复微信开发者工具 60 秒超时错误处理问题

- abort() 只清理资源,不再触发 onError(避免正常流程误报错误)
- 真正的错误(超时、网络错误)通过 Promise.race 的 catch 正确传递给用户
- 修复微信开发者工具强制终止 fetch 时 onError 不被调用的问题
- 使用 errorHandled 标志防止 onError 重复调用
This commit is contained in:
2026-07-23 22:21:46 +08:00
parent 6eadbce6f2
commit 1cf75a2b40
+33 -17
View File
@@ -212,8 +212,9 @@ function h5NovelStream(url, body, onEvent, onError) {
let reader = null let reader = null
let timeoutTimer = null let timeoutTimer = null
let isCompleted = false let isCompleted = false
let errorHandled = false // 防止 onError 重复调用
// 统一的清理函数 // 统一的清理函数(不处理错误,只清理资源)
const cleanup = () => { const cleanup = () => {
isCompleted = true isCompleted = true
if (timeoutTimer) clearTimeout(timeoutTimer) if (timeoutTimer) clearTimeout(timeoutTimer)
@@ -222,11 +223,19 @@ function h5NovelStream(url, body, onEvent, onError) {
} }
} }
// 安全的错误处理(确保 onError 只调用一次)
const safeOnError = (msg) => {
if (!errorHandled) {
errorHandled = true
cleanup()
onError?.(msg)
}
}
// 创建超时 Promise(不依赖 AbortController // 创建超时 Promise(不依赖 AbortController
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
timeoutTimer = setTimeout(() => { timeoutTimer = setTimeout(() => {
if (!isCompleted) { if (!isCompleted) {
cleanup()
reject(new Error('请求超时(300 秒)')) reject(new Error('请求超时(300 秒)'))
} }
}, SSE_TIMEOUT_MS) }, SSE_TIMEOUT_MS)
@@ -244,9 +253,7 @@ function h5NovelStream(url, body, onEvent, onError) {
}).then(response => { }).then(response => {
// 检查响应状态 // 检查响应状态
if (!response.ok) { if (!response.ok) {
cleanup() throw new Error(`请求失败:HTTP ${response.status}`)
onError?.(`请求失败:HTTP ${response.status}`)
return
} }
// 获取 ReadableStream 读取器 // 获取 ReadableStream 读取器
@@ -257,8 +264,14 @@ function h5NovelStream(url, body, onEvent, onError) {
// 循环读取流数据 // 循环读取流数据
function pump() { function pump() {
return reader.read().then(({ done, value }) => { return reader.read().then(({ done, value }) => {
if (done || isCompleted) { if (done) {
cleanup() // 流正常结束,标记为已完成(后续 abort 不触发错误)
isCompleted = true
if (timeoutTimer) clearTimeout(timeoutTimer)
return
}
if (isCompleted) {
return return
} }
@@ -275,9 +288,6 @@ function h5NovelStream(url, body, onEvent, onError) {
// 继续读取 // 继续读取
return pump() return pump()
}).catch(err => {
cleanup()
throw err
}) })
} }
@@ -285,17 +295,23 @@ function h5NovelStream(url, body, onEvent, onError) {
}) })
// 使用 Promise.race 竞争超时和 fetch // 使用 Promise.race 竞争超时和 fetch
Promise.race([fetchPromise, timeoutPromise]).catch(err => { Promise.race([fetchPromise, timeoutPromise]).then(() => {
if (!isCompleted) { // 成功完成,清理资源
cleanup() if (timeoutTimer) clearTimeout(timeoutTimer)
onError?.(err.message || '请求超时') if (reader) reader.cancel().catch(() => {})
} }).catch(err => {
// 任何错误都传递给用户(包括微信开发者工具的 60 秒强制终止)
safeOnError(err.message || '请求超时')
}) })
// 返回 abort 接口 // 返回 abort 接口(只清理资源,不触发错误)
return { return {
abort: () => { abort: () => {
cleanup() // abort 只负责取消请求和清理资源,不触发 onError
// 真正的错误(超时、网络错误)会通过 Promise.race 的 catch 触发
isCompleted = true
if (timeoutTimer) clearTimeout(timeoutTimer)
if (reader) reader.cancel().catch(() => {})
} }
} }
} }