超时配置与流式响应
invoke 默认 5 秒、执行上限 300 秒、返回体积约 1 MiB——长任务撞的到底是哪一条,以及 context deadline exceeded 的诊断顺序。含 ctx.sse.send 加原生 Fetch 的完整 SSE 解析写法,和「流式不是后台任务」这条边界。
普通 invoke() 等的是一个完整 JSON 结果,默认 5 秒就超时。模型生成、慢的第三方接口和需要边算边显示的场景都会撞上这条线。这篇讲两件事:把超时设在对的地方,以及用原生 Fetch + SSE 做有界的增量输出。
超时配置
invoke 默认超时是 5 秒,Func runner 的默认最大执行时间是 300 秒。有界的模型生成或第三方请求可以在调用端设置更长时间;这不意味着可以启动脱离请求继续执行的后台任务。
const result = await invoke('image.generate', input, {
timeoutMs: 120000,
})
同时生效的还有几条硬限制,它们不会因为放宽超时而变化:
| 限制 | 值 |
|---|---|
invoke 默认超时 | 5 秒,由调用端 timeoutMs 覆盖 |
| 单次执行上限 | 300 秒 |
| Func 返回体积 | 约 1 MiB,超出即失败——这是"把整表读出来返回"最先撞到的墙 |
| Func 代码体积 | 约 256 KiB |
诊断 context deadline exceeded
遇到 context deadline exceeded 或 context timeout 时,按顺序做三件事:
- 先检查页面实际传给
invoke(..., { timeoutMs })的值。绝大多数情况问题在这里——调用端还是默认的 5 秒。 - 用相同输入、相同模型提高超时重试。
creght func run --timeout_ms/run_func.timeout_ms只影响这次自测,跑通不能证明生产路径也会通,跑不通也不能证明平台有同样的硬限制。 - 如果提高后成功,就提高生产调用端的
timeoutMs,并在真实页面上验证一次完整路径。
不要把缩短输出、降低
max_tokens、切换模型或供应商、创建新表、伪造后台任务当成超时修复。这些都会改变功能本身,而问题只是一个数字设小了。
原生 Fetch + SSE 流式响应
需要有界增量输出时,服务端通过 ctx.sse.send 发事件,浏览器直接使用原生 fetch 和 ReadableStream;不需要 invokeStream 这样的封装。
// /backend/func/writer.ts
export async function main(input, ctx) {
ctx.sse.send('token', { text: 'Hello' })
ctx.sse.send('token', { text: ' world' })
return { ok: true }
}
浏览器侧要自己解析 SSE 帧。关键是 reader.read() 返回的是任意字节块,不保证一个块正好是一个事件:
const response = await fetch('/func/writer?stream=1&timeout_ms=120000', {
method: 'POST',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok || !response.body) throw new Error('Func stream failed')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const frames = buffer.split(/\r?\n\r?\n/)
buffer = frames.pop() || ''
for (const frame of frames) {
const event = frame.match(/^event:\s*(.+)$/m)?.[1]
const data = frame.match(/^data:\s*(.+)$/m)?.[1]
if (event === 'token' && data) {
output += JSON.parse(data).text
}
}
}
- 必须跨读取缓存,并且只按空行分隔 SSE frame,最后一段不完整的留在缓冲区里等下一块。
- 流式路径用 URL 上的
?stream=1&timeout_ms=...,不是invoke()的选项。 - 平台负责发最终的
done或error事件,两个都要处理。 - 第一个事件发出后不能再修改 Cookie:响应头此时已经提交。需要设置会话或 Cookie 的动作要放在第一次
send之前。 - 调用超时同样生效,流式不等于无限流。
流式不是后台任务
SSE 让用户更早看到进度,但它仍然是一次请求内的输出。Func 不支持脱离请求继续执行:没有 setTimeout/setInterval,没有任务队列,也没有"先返回再慢慢算"。
如果一件事确实超过 300 秒,就把它拆成用户可见的多步:每一步是一次有界调用,中间状态落在 JSON 表里,由页面驱动下一步——而不是让一次调用挂在那里。
验收清单
- 长耗时调用在生产页面上设置了与实际耗时匹配的
timeoutMs,并验证过真实路径。 - 返回值没有夹带大段内容,没有接近 1 MiB 的返回体。
- SSE 解析跨读取缓存,按空行切帧,处理了
done与error。 - 需要设置 Cookie 的逻辑在第一个 SSE 事件之前完成。
- 没有用计时器、轮询或伪造的后台任务来绕过超时。
