-32700 Parse error 表示服务器收到的字节不是有效 JSON,因此它从未校验 jsonrpc、method、params 或 id,并且错误响应始终携带 id null。因此故障出在传输或编码层,而不是应用逻辑:请求体被截断、在 keep-alive 连接上写入被拼接、载荷被双重编码、缺少 Content-Encoding 头、非 UTF-8 字节以及 Content-Type 不匹配都是常见原因。可靠的方法是记录实际发送的确切字节(长度加哈希)、实际应用的 Content-Type 与 Content-Encoding,以及原始响应体,然后用 curl 重放这些字节,使故障在应用之外可复现。切勿盲目重试 -32700;先修复编码器或代理并重建载荷。JSON-RPC 2.0 规范固定了错误码和 null id,但未固定 HTTP 状态码,因此需同时读取 HTTP 层和错误对象。
-32700 Parse Error 实际报告了什么
JSON-RPC 2.0 规范第 5.1.1 节将 -32700 Parse error 定义为服务器收到无效 JSON 时返回的错误码。这一句话承载了整个诊断结论:服务器尝试将请求体解析为 JSON,解析失败,并在检查请求对象的任何成员之前就停止了执行。没有 jsonrpc 版本检查、没有方法查找、没有 params 校验,也没有 id 提取。
由于无法检测到请求 id,错误响应携带 id null。这不是提供商的缺陷,而是规范规定的行为,也是你面对编码故障而非应用故障的最强信号。与之对比,验证失败如 JSON-RPC -32602 invalid params validation 中,服务器确实解析了信封、读取了你的 id 并将其回显。
同一规范第 6 节增加了批量请求的特定规则:如果批量请求体本身不是有效 JSON,服务器返回单个错误对象,而不是响应数组。空数组是有效 JSON,属于完全不同的情况,这也是批量形式的查询经常引出无关讨论的原因。关于请求体解析成功后适用的排序规则,请参阅 JSON-RPC batching best practices。
- 规范规定:收到无效 JSON 时返回 -32700(JSON-RPC 2.0,第 5.1.1 节)。
- 规范规定:错误响应使用 id null,因为无法检测到请求 id。
- 规范规定:请求体不是有效 JSON 的批量请求产生一个错误对象,而不是数组(第 6 节)。
- 规范规定:规范未强制要求此情况下的 HTTP 状态码。
为什么解析失败是传输与编码故障
应用错误发生在信封被理解之后:方法不存在、参数类型错误、合约调用回滚。解析错误发生在信封存在之前。线上的字节根本不是 JSON 文档,因此节点没有任何东西可以路由、计量或授权。这就是为什么以“我的交易失败了”开头的 -32700 工单通常是误标:根本没有构造过交易。
实际后果是,你应该先停止阅读应用代码,转而先阅读字节流。问题不是“我的代码打算发送什么”,而是“套接字实际承载了什么”。下面按频率排序的原因列表中的每一项都是字节级缺陷,一旦捕获字节,每一项都可复现。
这一区别也解释了为什么错误对象很单薄。没有包含堆栈跟踪的 data 成员,因为服务器没有上下文可描述。如果你想了解 code、message 和 data 通常如何关联,请参阅 Decoding the JSON-RPC error object。
按实际出现频率排序的生产环境原因
以下排序反映每种原因在真实事故报告中出现的频率。将其视为分诊顺序,而非统计声明:从顶部开始向下排查,因为前两个原因占了大多数情况——载荷在调试器中看起来正确,但在生产环境中失败。
截断排在首位,因为它在应用日志中不可见。代理、负载均衡器或客户端超时可能在请求体写入中途关闭连接,服务器收到的是有效 JSON 的前缀,但突然结束。拼接排在第二,因为重试逻辑经常在不丢弃第一次写入的情况下重新发送,导致 keep-alive 连接上两个请求体背靠背,解析器看到的是一个无效文档。
其余原因是编码缺陷:载荷被双重编码,服务器收到的是包含 JSON 的 JSON 字符串而非对象;发送二进制或压缩字节时缺少匹配的 Content-Encoding 头,服务器试图将 gzip 当作 UTF-8 解析;通过字符串拼接 JSON 构建器注入的非 UTF-8 字节序列;以及 Content-Type 与请求体不匹配,例如将表单编码数据 POST 到期望 application/json 的端点。
- 请求体在写入中途被代理、负载均衡器或客户端超时截断。
- 重试未丢弃第一次写入,导致两个请求体在同一个 keep-alive 连接上拼接。
- 载荷被双重编码:服务器收到的是包含 JSON 的 JSON 字符串而非对象。
- 发送压缩或二进制字节时缺少匹配的 Content-Encoding 头。
- 手动字符串拼接注入的非 UTF-8 字节序列。
- Content-Type 与请求体不匹配,例如将表单编码数据发送到 JSON 端点。
捕获与比对流程:将模糊错误转化为证据
只有当你能说出实际发送的确切字节时,-32700 报告才可操作。在套接字写入前的最后时刻对客户端进行插桩,而不是在构建请求对象的时刻,因为代理和 HTTP 库可能在你的代码完成后转换请求体。
同时记录四样东西:序列化请求体的字节长度、这些字节的哈希、实际应用到请求的 Content-Type 和 Content-Encoding 头,以及按原样收到的原始响应体。哈希很重要,因为它能证明你重放的字节就是失败的字节;原始响应体很重要,因为 WAF 可能返回 HTML 而不是 JSON-RPC 错误对象。
然后用 curl 重放记录的字节,包括相同的头,使故障在应用之外可复现。如果重放成功,缺陷在你的客户端栈或中间层;如果重放同样失败,你手中就有问题载荷,可以逐字节二分排查。
curl -sS -D - -o response.bin \
-X POST 'https://your-endpoint.example/rpc' \
-H 'Content-Type: application/json' \
--data-binary @captured-body.bin
# Inspect what came back, byte for byte
wc -c captured-body.bin response.bin
head -c 400 response.bin; echo
# Confirm the captured body is valid JSON before blaming the server
node -e "const fs=require('fs');const b=fs.readFileSync('captured-body.bin');try{JSON.parse(b.toString('utf8'));console.log('body parses locally')}catch(e){console.log('local parse failure:',e.message)}"可运行的 Node.js 示例:发送前先断言
最经济的永久修复是在客户端进行预检断言。用 JSON.stringify 序列化,再将结果解析回来,确认得到的是对象而不是字符串。双重编码的载荷能通过简单的真值检查,但会立即在这个往返测试中失败。
下面的示例还会打印原始错误载荷,包括 null id,这样你可以在同一日志流中看到解析失败与应用错误的区别。对你控制的任何端点运行它,包括来自 Ethereum network page 或你自己的 API service 部署的端点。
const endpoint = process.env.RPC_URL || 'https://your-endpoint.example/rpc';
function buildBody(method, params) {
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
const roundTrip = JSON.parse(body);
if (typeof roundTrip !== 'object' || roundTrip === null || Array.isArray(roundTrip)) {
throw new Error('Body did not round-trip to a JSON object; check for double encoding');
}
return body;
}
async function call(method, params) {
const body = buildBody(method, params);
const bytes = Buffer.byteLength(body, 'utf8');
const hash = require('crypto').createHash('sha256').update(body).digest('hex').slice(0, 16);
console.log('sending bytes=%d sha256=%s content-type=application/json', bytes, hash);
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
});
const raw = await res.text();
console.log('http status=%d raw=%s', res.status, raw.slice(0, 400));
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
console.error('Response is not JSON; the HTTP layer is the real signal here');
return;
}
if (parsed.error) {
console.error('rpc error code=%s message=%s id=%s', parsed.error.code, parsed.error.message, parsed.id);
if (parsed.error.code === -32700) {
console.error('Parse failure: the server never read a request object. Fix the encoder or proxy, then rebuild the payload.');
}
} else {
console.log('result=%o', parsed.result);
}
}
call('eth_blockNumber', []).catch((e) => console.error('transport failure:', e.message));区分客户端解析错误与服务器解析错误
最具误导性的 -32700 工单是读者自己的解析器出错的情况。如果响应体不是有效 JSON,你的客户端无法解码,由此产生的异常常被报告为“节点返回了解析错误”,而实际上节点返回的是 HTML、空响应体或截断的流。在归责之前,请双向阅读交换内容。
测试简单而机械:自己解析原始响应体。如果它能解析并包含 code 为 -32700 的错误对象,则服务器拒绝了你的请求字节。如果它无法解析,则故障在响应路径上,正确的信号是 HTTP 状态和头,而不是 JSON-RPC 错误对象。
这也是提供商行为存在差异的地方。JSON-RPC 语义由规范固定,但解析失败使用的 HTTP 状态码并未固定,一些提供商在端点前部署 Web 应用防火墙,返回 HTML 质询页面。在这种情况下,必须完全忽略错误对象。
- 响应可解析且包含 code -32700:服务器拒绝了你的请求字节。
- 响应无法解析:故障在响应路径上,因此读取 HTTP 状态和头。
- 响应是 HTML:WAF 等中间层作出了应答,JSON-RPC 错误对象不存在。
- 响应为空:怀疑截断或连接重置,而不是 JSON 缺陷。
重试语义:为什么盲目重试无法成功
-32700 相对于导致它的字节是确定性的。相同的字节在下一次尝试、再下一次尝试中都会解析失败,因此重新发送未更改缓冲区的重试循环会将单次失败转化为持续的错误率,并可能放大端点负载。这与瞬时网络故障相反,后者重试是正确的响应。
正确的做法是修复编码器或中间层,然后从头重建载荷。只有在请求体被重新生成、重新序列化并重新断言之后,才应发出重试。如果你的重试逻辑位于共享 HTTP 客户端中,请添加一个守卫,当响应包含 code -32700 时拒绝重试。
作为对比,瞬时内部故障如 JSON-RPC -32603 internal error debugging 可以合理地使用退避重试,因为请求已被理解,故障发生在下游。区别在于服务器是否读取过你的请求对象。
结果表:来自你自己端点的可复现证据
构建一个表格,每行一个故意损坏的载荷,并针对你自己的端点运行。这将轶事性错误转化为可复现的矩阵,也揭示了你特定提供商如何将解析失败映射到 HTTP 状态码,而规范对此未作规定。
用你观察到的值填写下面的列。不要从任何文章(包括本文)复制数字:这个练习的重点是证据来自你的端点、你的代理链和你的客户端栈。
- 应用的损坏类型:截断请求体、拼接请求体、双重编码字符串、无 Content-Encoding 的 gzip、无效 UTF-8 字节、表单编码请求体。
- 服务器错误码:观察到的 JSON-RPC 错误码,解析类情况预期为 -32700。
- 消息:返回的确切消息字符串,逐字引用。
- HTTP 状态:观察到的状态行,根据提供商可能是 400、200 或 500。
- 回显的 id:响应中的 id 成员,解析失败预期为 null。
- 耗时毫秒:从写入到完整读取响应的墙上时钟时间,由你的客户端测量。
| Corruption applied | Server code | Message | HTTP status | id echoed | Elapsed ms |
| --- | --- | --- | --- | --- | --- |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |解析错误诊断的局限与权衡
规范有意不固定解析失败的 HTTP 状态码。一些端点返回 400,一些返回 200 并在响应体中包含错误对象,还有一些返回 500。任何仅依据 HTTP 状态进行错误处理的客户端都会至少误判其中一种,因此需显式处理两层。
提供商在边缘的行为也各不相同。Web 应用防火墙或网关可能在畸形请求体到达 JSON-RPC 处理器之前拦截它,并返回 HTML、重定向或质询页面。在这种情况下,JSON-RPC 错误对象不存在,不得合成;HTTP 层是唯一可靠的信号。
最后,字节级捕获也有其权衡。记录完整请求体可能暴露敏感参数并增加日志量,因此生产环境中优先记录长度加哈希,仅在受控调试窗口保留完整请求体。哈希足以证明重放与原始失败匹配。
实时 -32700 事故排查清单
按顺序执行清单,一旦重放复现或清除故障就停止。每一项都旨在排除一个层面,而不是猜测原因。
如果使用相同字节和头重放成功,缺陷在你的客户端栈或客户端与端点之间的中间层。如果同样失败,则二分捕获的请求体:切成两半,分别测试,继续直到隔离出问题字节范围。一个杂散字节就足以使整个文档无效。
- 在联系服务器之前,用严格的 JSON 解析器确认捕获的请求体在本地可解析。
- 确认 Content-Type 与请求体匹配,Content-Encoding 与实际编码匹配。
- 确认没有代理缓冲、重写或截断请求体;检查其请求体大小限制。
- 确认重试逻辑在 keep-alive 连接上重新发送前丢弃了上一次写入。
- 在解释响应体中的任何错误码之前,确认响应体是 JSON。
- 确认端点 URL 和方法正确;对仅支持 POST 的端点执行 GET 在某些网关可能表现为解析错误。
后续步骤与相关阅读
解析失败修复后,加固客户端,使同类缺陷不再复发:保留往返断言、保留长度与哈希日志行,并添加在 -32700 时拒绝重试的守卫。然后审查端点的访问路径,因为大多数解析失败源于路径而非载荷构建器。
关于端点选择和连接模式,请从 RPC endpoints guide 和更广泛的 OnFinality Learn hub 开始。如果你正在为客户端产生的流量规划容量,RPC pricing 页面描述了商业选项,Ethereum network page 列出了你可以测试的端点。
关于权威协议语义,请阅读 https://www.jsonrpc.org/specification 上的 JSON-RPC 2.0 规范,特别是第 5.1.1 节和第 6 节,以及 https://ethereum.github.io/execution-apis/ 上的 Ethereum JSON-RPC 规范,了解这些错误码在执行层端点上的实际表现。两者都是主要来源;提供商特定行为(如 HTTP 状态映射和 WAF 拦截)按提供商记录且各不相同。