如何在Node.js中向客户端返回OpenAI流式响应
问题核心
在Node.js环境中调用OpenAI流式API时,虽然能正常获取分片数据,但使用toJSON(stream)序列化流返回给客户端会导致接收结果异常——客户端拿到的是流的结构描述,而非实际的流式文本内容。这是因为ReadableStream是不可序列化的对象,无法通过JSON转换传递实际数据。
错误原因
你当前的代码中使用res.status(200).send(toJSON(stream)),本质上是把Web标准的ReadableStream对象序列化为JSON字符串,客户端收到的只是流的元数据(比如内部状态、构造函数信息),而非逐步推送的文本分片。
而Next.js的API路由支持直接返回new Response(stream),是因为Next.js会自动将Web标准流适配为HTTP响应的分块传输,无需手动序列化。
解决方案
在Node.js中,需要将Web标准的ReadableStream转换为Node.js原生的Readable流,然后通过管道(pipe)传输给客户端,并设置正确的响应头确保流式传输生效。
步骤1:转换Web流到Node.js流
添加一个转换函数,将Web的ReadableStream转换为Node.js的stream.Readable:
import { Readable } from 'stream'; import { ReadableStream } from 'web-streams-polyfill/ponyfill/es2018'; /** * 将Web标准ReadableStream转换为Node.js原生Readable流 * @param webStream Web标准ReadableStream * @returns Node.js Readable流 */ function webStreamToNodeStream(webStream) { const reader = webStream.getReader(); const nodeStream = new Readable({ async read() { try { const { done, value } = await reader.read(); if (done) { this.push(null); return; } this.push(value); } catch (err) { this.destroy(err); } }, }); return nodeStream; }
步骤2:修改接口处理函数
移除toJSON序列化,改用管道传输流,并设置正确的响应头:
// 移除import { toJSON } from 'flatted'; export const fetchChatOpenAI = async (req, res) => { try { const stream = await OpenAIStream( model, promptToSend, temperatureToUse, key, messagesToSend ); // 设置流式响应的必要头信息 res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.setHeader('Transfer-Encoding', 'chunked'); // 如果使用Server-Sent Events(SSE)协议,替换为以下头信息: // res.setHeader('Content-Type', 'text/event-stream'); // res.setHeader('Cache-Control', 'no-cache'); // res.setHeader('Connection', 'keep-alive'); // 转换流并管道传输给客户端 const nodeStream = webStreamToNodeStream(stream); nodeStream.pipe(res); // 处理流结束和错误 nodeStream.on('end', () => res.end()); nodeStream.on('error', (err) => { console.error(err); res.status(500).end('Stream transmission failed'); }); } catch (error) { if (error instanceof OpenAIError) { console.error(error); res.status(500).json({ statusText: error.message }); } else { res.status(500).json({ statusText: 'ERROR' }); } } };
步骤3:客户端代码无需修改
你当前的客户端代码已经正确处理了流式响应(通过response.body.getReader()读取分片),保持现有逻辑即可。
验证效果
修改后,客户端会逐步接收到OpenAI返回的文本分片,日志输出会和Next.js示例一致——每次reader.read()都会返回对应的文本块,而非序列化后的流对象。
内容的提问来源于stack exchange,提问作者texas697

