Next.js生产环境Vercel Edge Function流式数据解析错误
问题
在Next.js应用中,通过Vercel Edge Function流式获取数据时,本地开发环境运行正常,但生产环境出现JSON解析错误,控制台日志如下:
SyntaxError: JSON.parse: unterminated string at line 1 column 23 of the JSON data.
生产环境中,流数据可能因网络延迟、物理基础设施距离等因素出现碎片化,这与Vercel官方流式功能注意事项的描述一致。
目前已实现累加器逻辑,通过换行符分割处理JSON字符串,本地运行正常,但生产环境仍无法解决解析错误,导致流式数据无法渲染。现有处理代码片段:
// ... // Temporary variable to hold the assistant's response let assistantResponse = ''; let jsonAccumulator = ''; // Accumulator for reconstructing JSON strings const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) { break; } jsonAccumulator += decoder.decode(value, { stream: true }); // Process JSON strings whenever a new line is encountered while (jsonAccumulator.includes('\n')) { const lineEnd = jsonAccumulator.indexOf('\n'); const line = jsonAccumulator.slice(0, lineEnd).trim(); jsonAccumulator = jsonAccumulator.slice(lineEnd + 1); if (line.startsWith('data: ')) { const jsonChunkStr = line.substring(6); if (jsonChunkStr.trim() === '[DONE]') { break; } else { try { const jsonChunk = JSON.parse(jsonChunkStr); if (jsonChunk.choices && jsonChunk.choices[0].delta.content) { assistantResponse += jsonChunk.choices[0].delta.content; setMessages((prevMessages) => [ ...prevMessages.slice(0, -1), { role: 'assistant', content: assistantResponse }, ]); } } catch (error) { console.error('JSON parse error:', error); console.error('Chunk string:', jsonChunkStr); } } } } } // ...
稳健解决方案
核心问题是生产环境中,单条data:行可能被拆分成多个read()返回块,仅靠换行符分割的逻辑无法处理半行内容。以下是优化后的方案:
// ... let assistantResponse = ''; let jsonAccumulator = ''; const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) { // 处理流结束后剩余的未换行内容 if (jsonAccumulator.trim()) { processAccumulatedLine(jsonAccumulator); } break; } jsonAccumulator += decoder.decode(value, { stream: true }); // 分割所有完整行,未完成的部分留回累加器 const lines = jsonAccumulator.split('\n'); jsonAccumulator = lines.pop() || ''; // 逐行处理完整内容 for (const line of lines) { processAccumulatedLine(line.trim()); } } // 提取单条行处理逻辑为独立函数 function processAccumulatedLine(line) { if (!line.startsWith('data: ')) return; const jsonChunkStr = line.substring(6).trim(); if (jsonChunkStr === '[DONE]') return; try { // 预检查JSON是否完整,避免无效解析 if (isValidJsonCandidate(jsonChunkStr)) { const jsonChunk = JSON.parse(jsonChunkStr); if (jsonChunk.choices && jsonChunk.choices[0].delta?.content) { assistantResponse += jsonChunk.choices[0].delta.content; setMessages(prev => [ ...prev.slice(0, -1), { role: 'assistant', content: assistantResponse }, ]); } } else { // 不完整的块放回累加器,等待后续内容补全 jsonAccumulator = `${line}\n${jsonAccumulator}`; } } catch (error) { console.error('JSON解析失败:', error); console.error('当前块内容:', jsonChunkStr); // 解析失败时将块放回累加器,避免数据丢失 jsonAccumulator = `${line}\n${jsonAccumulator}`; } } // 简单判断JSON是否完整(适配常见流式返回格式) function isValidJsonCandidate(str) { str = str.trim(); if (!str) return false; // 检查首尾符号匹配 const firstChar = str[0]; const lastChar = str[str.length - 1]; if ((firstChar === '{' && lastChar !== '}') || (firstChar === '"' && lastChar !== '"')) { return false; } // 统计引号和大括号的闭合数量 let quoteCount = 0; let braceCount = 0; for (let i = 0; i < str.length; i++) { const char = str[i]; if (char === '"' && (i === 0 || str[i - 1] !== '\\')) { quoteCount++; } else if (char === '{') { braceCount++; } else if (char === '}') { braceCount--; } } return quoteCount % 2 === 0 && braceCount === 0; } // ...
关键优化点
- 行分割逻辑改进:用
split('\n')分割所有完整行,剩余未完成的半行内容留回累加器,避免截断有效数据。 - 流结束收尾:确保最后一行即使没有换行符也能被处理。
- 完整性预检查:解析前先验证引号、大括号的闭合情况,减少不必要的解析错误,同时将不完整块放回累加器等待补全。
- 错误恢复机制:解析失败时将当前块放回累加器,后续收到补全内容后可重新处理,避免数据丢失。
内容的提问来源于stack exchange,提问作者Martin Erlic
相关产品推荐
相关产品推荐

