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

- 将 AbortController 方案改为 Promise.race + setTimeout 外部超时控制
- 解决微信开发者工具底层 fetch 不兼容 AbortController signal 的问题
- 保持 300 秒超时时间不变
- 简化 abort 接口,统一使用 cleanup 函数
This commit is contained in:
2026-07-23 22:12:42 +08:00
parent bd706e4a5b
commit 6eadbce6f2
+50 -45
View File
@@ -194,14 +194,14 @@ function h5ConsumeSseText(block, onEvent, onError) {
const event = JSON.parse(dataBuffer) const event = JSON.parse(dataBuffer)
onEvent?.(event) onEvent?.(event)
} catch (e) { } catch (e) {
onError?.(`SSE 解析失败: ${e.message}`) onError?.(`SSE 解析失败${e.message}`)
} }
} }
} }
/** /**
* H5 环境 SSE 流式读取核心实现 * H5 环境 SSE 流式读取核心实现
* 使用浏览器原生 fetch + ReadableStream + AbortController * 使用 Promise.race + setTimeout 实现外部超时控制(兼容微信开发者工具)
* @param {string} url - 请求 URL * @param {string} url - 请求 URL
* @param {Object} body - 请求体 * @param {Object} body - 请求体
* @param {Function} onEvent - 事件回调 * @param {Function} onEvent - 事件回调
@@ -209,63 +209,63 @@ function h5ConsumeSseText(block, onEvent, onError) {
* @returns {Object} 包含 abort 方法的对象 * @returns {Object} 包含 abort 方法的对象
*/ */
function h5NovelStream(url, body, onEvent, onError) { function h5NovelStream(url, body, onEvent, onError) {
// 1. 创建 AbortController 用于超时控制 let reader = null
const controller = new AbortController() let timeoutTimer = null
let abortedByUser = false // 标记是否用户主动中止 let isCompleted = false
let reader = null // 引用 reader 以便主动中止时取消
const timeoutId = setTimeout(() => controller.abort(), SSE_TIMEOUT_MS)
// 2. 统一的错误处理函数 // 统一的清理函数
const handleError = (err) => { const cleanup = () => {
clearTimeout(timeoutId) isCompleted = true
if (abortedByUser) { if (timeoutTimer) clearTimeout(timeoutTimer)
// 用户主动中止,不报错 if (reader) {
return reader.cancel().catch(() => {})
}
if (err && err.name === 'AbortError') {
onError?.('请求超时(300秒)')
} else {
onError?.(err?.message || '网络请求失败')
} }
} }
// 3. 发起 fetch 请求 // 创建超时 Promise(不依赖 AbortController
fetch(url, { const timeoutPromise = new Promise((_, reject) => {
timeoutTimer = setTimeout(() => {
if (!isCompleted) {
cleanup()
reject(new Error('请求超时(300 秒)'))
}
}, SSE_TIMEOUT_MS)
})
// 创建 fetch Promise
const fetchPromise = fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'text/event-stream', 'Accept': 'text/event-stream',
...getAuthHeader() ...getAuthHeader()
}, },
body: JSON.stringify(body), body: JSON.stringify(body)
signal: controller.signal
}).then(response => { }).then(response => {
// 4. 检查响应状态 // 检查响应状态
if (!response.ok) { if (!response.ok) {
clearTimeout(timeoutId) cleanup()
onError?.(`请求失败: HTTP ${response.status}`) onError?.(`请求失败HTTP ${response.status}`)
return return
} }
// 5. 获取 ReadableStream 读取器 // 获取 ReadableStream 读取器
reader = response.body.getReader() reader = response.body.getReader()
const decoder = new TextDecoder('utf-8') const decoder = new TextDecoder('utf-8')
let buffer = '' let buffer = ''
// 6. 循环读取流数据 // 循环读取流数据
function pump() { function pump() {
return reader.read().then(({ done, value }) => { return reader.read().then(({ done, value }) => {
if (done) { if (done || isCompleted) {
clearTimeout(timeoutId) cleanup()
// 处理缓冲区残留数据
if (buffer.trim()) h5ConsumeSseText(buffer, onEvent, onError)
return return
} }
// 7. 解码二进制块为文本 // 解码二进制块为文本
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true })
// 8. 解析完整的事件(兼容 CRLF // 解析完整的事件(兼容 CRLF)
const events = buffer.split(/\r?\n\r?\n/) const events = buffer.split(/\r?\n\r?\n/)
buffer = events.pop() // 最后一个可能不完整,保留到下次 buffer = events.pop() // 最后一个可能不完整,保留到下次
@@ -273,24 +273,29 @@ function h5NovelStream(url, body, onEvent, onError) {
h5ConsumeSseText(event, onEvent, onError) h5ConsumeSseText(event, onEvent, onError)
} }
// 9. 继续读取 // 继续读取
return pump() return pump()
}).catch(err => {
cleanup()
throw err
}) })
} }
return pump() return pump()
}).catch(handleError) })
// 10. 返回 abort 接口 // 使用 Promise.race 竞争超时和 fetch
Promise.race([fetchPromise, timeoutPromise]).catch(err => {
if (!isCompleted) {
cleanup()
onError?.(err.message || '请求超时')
}
})
// 返回 abort 接口
return { return {
abort: () => { abort: () => {
clearTimeout(timeoutId) cleanup()
abortedByUser = true
// 主动中止时取消 reader,释放底层资源
if (reader) {
reader.cancel().catch(() => {})
}
controller.abort()
} }
} }
} }
@@ -314,8 +319,8 @@ function consumeSseText(text, onEvent, onError) {
const event = JSON.parse(dataStr) const event = JSON.parse(dataStr)
onEvent?.(event) onEvent?.(event)
} catch (e) { } catch (e) {
onError?.(`SSE 解析失败: ${e.message}`) onError?.(`SSE 解析失败${e.message}`)
} }
} }
} }
} }