You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 17:07:53