JSON-RPC -32600 Invalid Request 表示服务器已经把你的请求体解析为 JSON,但解析出的值不满足 JSON-RPC 2.0 请求对象契约。规范定义了严格的校验顺序:请求体必须能解析(否则返回 -32700 Parse error),解析出的值必须是 Object 或非空 Array(否则返回 -32600),随后信封成员必须满足第 4 节(否则返回 -32600),只有在此之后才会检查 params(否则返回 -32602 Invalid params)。大多数生产环境中的 -32600 事故来自:被截断或拼接后仍能解析的请求体、生产者发出 jsonrpc: "1.0" 或完全省略该成员、以及手工拼装的批量请求发送了空数组或非对象元素。本文提供一个可运行的 Node.js 信封校验器、一份 CI 预检清单,以及一张可复现的结果表,让你能针对自己的端点验证行为,而不是轻信一篇博客。
JSON-RPC 2.0 规范中的 -32600 Invalid Request 错误码
JSON-RPC 2.0 规范在第 5.1.1 节中将 -32600 Invalid Request 定义为五个预定义错误码之一,另外四个是 -32700 Parse error、-32601 Method not found、-32602 Invalid params 和 -32603 Internal error。该错误码专门用于服务器收到一个能解析为 JSON、但不是合法 Request 对象的值的情况。它不是传输错误,不是认证错误,也不是方法级失败——它描述的是你发送的信封的形状。
同一规范的第 4 节定义了 Request 对象:成员 jsonrpc 必须恰好是字符串 "2.0",成员 method 必须是 String,可选成员 params 必须是结构化值(Array 或 Object),可选成员 id 必须是 String、Number 或 Null。严格校验的服务器会在检查方法是否存在、参数是否正确之前,先以 -32600 拒绝任何偏离这些约束的请求。
以太坊 JSON-RPC 规范在这一信封之上叠加了具体的方法面(eth_call、eth_getBalance、eth_sendRawTransaction 等),但并未改变信封规则。同样的 jsonrpc、method、params 和 id 契约适用于每一个以太坊节点端点,因此针对基础规范编写的校验器可以跨服务商通用。若想更全面地了解以太坊端点,请参阅以太坊网络页面。
- 权威来源:JSON-RPC 2.0 规范,第 4 节(Request 对象)与第 5.1.1 节(预定义错误码),https://www.jsonrpc.org/specification
- 权威来源:以太坊 JSON-RPC 规范,https://ethereum.github.io/execution-apis/api-docs/
- 已记录行为:当解析出的值不是合法 Request 对象时返回 -32600;它与 -32700(请求体无法解析)和 -32602(信封合法但 params 非法)不同。
校验顺序:为什么你收到的错误码是诊断结论,而不是抽奖
符合规范的服务器不会一次性评估所有规则,而是按固定顺序逐条检查,第一条失败的规则决定你看到的错误码。正是这种顺序让错误码成为诊断结论:-32700 告诉你字节流从未变成 JSON,-32600 告诉你 JSON 不是合法信封,-32602 告诉你信封没问题但参数不对。
顺序如下:第一,请求体必须能解析为 JSON——如果不能,服务器返回 -32700 Parse error。第二,解析出的值必须是 Object 或非空 Array——如果是裸字符串、数字、布尔值或空数组,服务器返回 -32600 Invalid Request。第三,该对象(或数组中每个元素)的成员必须满足第 4 节——如果 jsonrpc 缺失或不是 "2.0",method 缺失或不是字符串,params 存在但不是 Array 或 Object,或 id 存在但不是 String、Number 或 Null,服务器返回 -32600。只有第四步,在信封合法之后,才会根据方法签名检查 params,此时不匹配会产生 -32602。
这个顺序之所以重要,是因为它告诉你该去哪里排查。如果你收到 -32600,问题出在你的序列化或客户端库,而不是 ABI 编码。如果你收到 -32602,信封没问题,问题在参数上。配套文章 JSON-RPC -32602 invalid params 参数校验 详细介绍了第二阶段。
- 请求体无法解析为 JSON → -32700 Parse error
- 解析出的值不是 Object 或非空 Array → -32600 Invalid Request
- 信封成员违反第 4 节 → -32600 Invalid Request
- 信封合法但 params 与方法不匹配 → -32602 Invalid params
一个可运行的 Node.js 信封校验器,能预测服务器返回的错误码
让 -32600 在编码阶段就不可能发生的最可靠方法,是在请求被序列化并发送之前,在你自己的进程内校验信封。下面的校验器会返回服务器本应返回的确切规范错误码,因此失败的测试会告诉你违反了哪条规则,而不是让你从生产日志里猜测。
该函数检查 jsonrpc === "2.0",不允许首尾空白,也不允许 "2" 或 "1.0" 之类的版本漂移;要求 method 是非空字符串;只接受 params 为 Array 或普通 Object(或不存在);只接受 id 为字符串、数字或 null。小数会被标记为警告而非硬性失败,因为规范不鼓励但并未禁止小数。在你的单元测试中针对应用能构造出的每一个请求运行它。
// envelope-validator.js — predicts the JSON-RPC 2.0 error code for a request envelope
function validateEnvelope(value) {
// Rule 1: parsed value must be an Object or a non-empty Array
if (Array.isArray(value)) {
if (value.length === 0) {
return { code: -32600, message: 'Invalid Request', reason: 'empty batch array' };
}
return value.map((el, i) => ({ index: i, ...validateEnvelope(el) }));
}
if (value === null || typeof value !== 'object') {
return { code: -32600, message: 'Invalid Request', reason: 'not an object' };
}
// Rule 2: jsonrpc must be exactly the string "2.0"
if (value.jsonrpc !== '2.0') {
return { code: -32600, message: 'Invalid Request', reason: 'jsonrpc must be exactly "2.0"' };
}
// Rule 3: method must be a non-empty string
if (typeof value.method !== 'string' || value.method.length === 0) {
return { code: -32600, message: 'Invalid Request', reason: 'method must be a non-empty string' };
}
// Rule 4: params, if present, must be an Array or a plain Object
if ('params' in value) {
const p = value.params;
const isPlainObject = p !== null && typeof p === 'object' && !Array.isArray(p);
if (!Array.isArray(p) && !isPlainObject) {
return { code: -32600, message: 'Invalid Request', reason: 'params must be an Array or Object' };
}
}
// Rule 5: id, if present, must be a String, Number, or Null
if ('id' in value) {
const id = value.id;
const ok = id === null || typeof id === 'string' || typeof id === 'number';
if (!ok) {
return { code: -32600, message: 'Invalid Request', reason: 'id must be String, Number, or Null' };
}
if (typeof id === 'number' && !Number.isInteger(id)) {
return { code: -32600, message: 'Invalid Request', reason: 'fractional id is discouraged', warning: true };
}
}
return { code: 0, message: 'valid envelope' };
}
module.exports = { validateEnvelope };
// Example usage in a test:
// const { validateEnvelope } = require('./envelope-validator');
// const result = validateEnvelope({ jsonrpc: '2.0', method: 'eth_blockNumber', params: [], id: 1 });
// console.assert(result.code === 0, result);生产环境中 -32600 最常见的两个原因
第一个原因是请求体仍能解析为 JSON,但不是单个对象。两个 JSON 对象首尾相连——例如 {"jsonrpc":"2.0",...}{...}——往往会被宽松的解析器接受(读取第一个值并忽略尾部字节),也可能被直接拒绝,取决于解析器。请求体被包裹在换行分隔的流中、客户端把多个请求拼接进一个 HTTP 请求体,也会产生同一类失败。服务器看到的值不是单个 Request 对象,于是返回 -32600。
第二个原因是生产者发出 "1.0" 之类的版本,或完全省略 jsonrpc 成员,而客户端库对开发者隐藏了信封。这最常发生在手工编写的 HTTP 客户端、代理或中间件自行构造请求体,而应用代码只提供 method 和 params 的时候。修复方法是在构造信封的边界处断言信封,而不是在消费它的地方。
这两个原因有共同特征:请求在应用代码里看起来正确,错误只出现在严格服务器上,而同样的载荷在宽松服务器上却能工作。正是这种不对称性,说明信封校验应该放在 CI 中,而不是事后复盘里。JSON-RPC 错误对象解码指南 介绍了错误到达后如何读取 code、message 和 data 字段。
- 拼接或换行分隔的请求体能解析但不是单个对象
- 生产者发出 jsonrpc: "1.0" 或完全省略该成员
- 中间件或代理重写请求体却没有重新校验信封
- 客户端库隐藏信封,只暴露 method 和 params
批量请求如何改变故障面
批量请求是 Request 对象组成的 Array,规范将空数组本身视为 Invalid Request:服务器返回单个错误对象,code 为 -32600,id 为 null。这是有记录的行为,不是服务商的怪癖,它会坑到那些动态构建批量请求、偶尔产生空列表的团队。
对于非空批量请求,规范要求服务器独立处理每个元素。单个元素数组如果不是合法 Request 对象,会为该元素产生一条 Invalid Request 条目,而不是让整个批量失败。这类 bug 正是手工拼装批量请求不可靠的原因:一个畸形元素会悄悄污染响应数组,而要把错误关联回出错的请求,就需要 id 字段存在且类型正确。配套文章 JSON-RPC id 关联与批量请求顺序 介绍了当部分元素失败时如何把响应映射回请求。
实用规则是:用与单个请求相同的校验器校验批量请求的每个元素,并在序列化之前拒绝空批量。如果你的批量构建器能产生空数组,它就能产生 -32600,而且错误会带着 id null 到达,很难归因到具体调用方。
- 空数组 [] → 单个 -32600 响应,id 为 null
- 非空数组 → 每个元素独立校验
- 一个非法元素 → 一条 Invalid Request 条目,而不是整批失败
- 用与单个请求相同的信封校验器校验每个元素
为什么 id 类型也是信封契约的一部分
id 成员是可选的,但一旦存在,它必须是 String、Number 或 Null。null id 只对响应以及服务器无法检测 id 的请求合法——例如在服务器读取 id 之前就未通过信封校验的请求。客户端如果生成对象 id,比如 { id: { requestId: 1 } },即使信封其余部分正确,也会在严格服务器上产生 Invalid Request。
规范明确不鼓励小数。服务器可能接受 id: 1.5,也可能拒绝;规范不保证任何一种行为,因此依赖它是可移植性隐患。请使用整数或字符串作为 id,并在批量请求内保持唯一,以便可靠地关联响应。
id 类型还会影响你如何解读错误。当服务器对无法检测 id 的请求返回 -32600 时,错误响应携带 id null。当服务器对 id 可读但信封其他部分非法的请求返回 -32600 时,错误响应可能会回显该 id。在排查批量失败、需要知道服务器能否归因错误时,这一区别很有用。
- 合法 id 类型:String、Number、Null
- 对象 id 非法,会在严格服务器上产生 -32600
- 不鼓励小数,可能被拒绝
- 错误响应中的 id null 通常意味着服务器无法检测请求 id
面向 CI 的规范化与预检清单
预检清单把信封校验从调试工作变成构建时门禁。把以下检查加入测试套件,让畸形信封永远到不了网络。每项检查都对应规范中的一条规则,因此失败会准确告诉你违反了哪条规则。
这份清单刻意保持精简。它覆盖规范校验的成员、容易出错的批量场景,以及拼接 bug 出现的序列化边界。如果你的应用在多个地方构造请求,请对每个构造点运行清单,而不只是主要的那一个。
- 断言 jsonrpc === "2.0",且没有首尾空白
- 断言 method 是非空字符串
- 断言 params 不存在、是 Array 或普通 Object
- 断言 id 不存在、是 String、整数 Number 或 Null
- 断言批量请求是非空 Array,且每个元素都通过相同检查
- 断言序列化后的请求体恰好包含一个 JSON 值,没有尾部字节
- 断言 Content-Type 头为 application/json,以便服务器按 JSON 解析请求体
- 在单元测试中针对应用能构造出的每个请求运行校验器
可复现的测量:针对你自己的端点填写结果表
有记录的行为告诉你规范要求什么,但不会告诉你你的具体服务商返回什么。要获得可复现的证据,请针对你自己的端点逐行重放一个故意破坏的信封,并记录服务器错误码、服务器消息、HTTP 状态码和耗时。下表是模板——请用你自己的测量结果填写,而不是轻信任何文章(包括本文)中的数字。
把每个变体作为原始 HTTP POST 发送,Content-Type 为 application/json,并捕获完整响应体。有些服务商会返回 HTTP 400,且响应体根本不是 JSON-RPC 错误对象,这就是为什么 HTTP 状态码列和错误码列同样重要。如果你在比较服务商,请对每个端点运行同一张表,并把原始响应与表格一起保存。RPC 端点指南 介绍了如何获取和配置端点以进行这类比较。
- 破坏变体:jsonrpc: "1.0" —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
- 破坏变体:省略 jsonrpc 成员 —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
- 破坏变体:method 是数字 —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
- 破坏变体:params 是字符串 —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
- 破坏变体:id 是对象 —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
- 破坏变体:两个 JSON 对象拼接 —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
- 破坏变体:空批量数组 [] —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
- 破坏变体:批量请求含一个非法元素 —— 记录服务器错误码、消息、HTTP 状态码、耗时 ms
| Input variant | Observed code | Observed message | HTTP status | Elapsed ms |
| --- | --- | --- | --- | --- |
| jsonrpc: "1.0" | ____ | ____ | ____ | ____ |
| jsonrpc omitted | ____ | ____ | ____ | ____ |
| method is a number | ____ | ____ | ____ | ____ |
| params is a string | ____ | ____ | ____ | ____ |局限性、服务商差异与传输层校验
规范将 -32000 到 -32768 范围保留给实现自定义错误,因此服务商对同一情况可以返回非标准错误码。对畸形信封返回 -32000 的服务器并未违反规范,它只是使用了实现自定义范围。这意味着你的错误处理不能假设 -32600 是畸形信封唯一可能产生的错误码。把错误码当作强信号,而不是保证。
有些服务商会返回 HTTP 400,且响应体根本不是 JSON-RPC 错误对象——例如负载均衡器返回的 HTML 错误页或纯文本消息。这是传输层失败,不是信封层失败,必须单独处理。你的客户端应在尝试把响应体解析为 JSON-RPC 响应之前检查 HTTP 状态码和 Content-Type,并在响应体不是 JSON-RPC 错误对象时抛出独立的错误类别。
服务商特定行为各不相同。规范定义了信封契约,但没有强制规定特定的 HTTP 状态码、特定的消息字符串,或对小数 id 等边界情况的特定处理方式。本文描述服务商行为的地方,请视为有记录或因服务商而异,并使用上面的结果表针对你自己的端点进行验证。若想更全面地讨论如何在服务商之间选择,请参阅 RPC 定价 和 API 服务 页面。
- -32000 到 -32768 范围保留给实现自定义错误;服务商可能对同一情况使用它
- 有些服务商返回 HTTP 400 且响应体不是 JSON-RPC;请单独处理传输错误
- 消息字符串和 HTTP 状态码因服务商而异,且未被规范规定
- 针对你自己的端点验证服务商行为,而不是假设只有单一错误码
生产环境 -32600 的故障排查流程
当生产环境出现 -32600 时,最快的修复路径是捕获原始请求体并用校验器重放。如果校验器返回 -32600,bug 在你的序列化或客户端库。如果校验器返回 0,bug 在传输层——某个代理、中间件或负载均衡器重写了请求体——你应该检查线上的字节,而不是应用代码里的对象。
如果校验器返回 -32700,请求体从未解析为 JSON,通常意味着截断或 Content-Type 不匹配。如果返回 -32602,信封没问题,问题在参数上,那是另一项排查。配套文章 解码以太坊 revert 原因与自定义错误 介绍了合法信封背后的方法级失败。
对于批量失败,检查错误响应是否携带 id null。如果是,服务器无法把错误归因到具体请求,通常意味着批量本身畸形——空数组,或请求体根本不是数组。如果错误响应回显了 id,说明批量可读,只是某个元素非法;逐个校验元素即可找到它。
- 捕获原始请求体,而不是应用对象
- 用校验器重放请求体以定位失败
- 校验器返回 -32600 → 序列化或客户端库 bug
- 校验器返回 -32700 → 截断或 Content-Type 不匹配
- 校验器返回 -32602 → 信封没问题,参数有误
- 错误响应携带 id null → 批量本身畸形
下一步:把信封校验纳入构建流程
目标是把 -32600 从生产事故变成失败的单元测试。把本文的校验器加入测试套件,针对应用能构造出的每个请求运行它,并在返回非零错误码时让构建失败。把结果表加入服务商评估清单,这样在依赖某个端点之前,你就有可复现的证据说明它的行为。
如果你在以太坊上开发,请从以太坊网络页面开始确认方法面,并使用 OnFinality Learn 中心查找关于错误对象、params 校验和 id 关联的配套文章。关于端点配置和服务商选择,RPC 端点指南 和 RPC 定价 页面覆盖了运维方面。API 服务 页面介绍了托管端点方案,如果你不想自己运行节点的话。
信封校验代码量很小,回报却很大:它消除了一整类生产错误,让剩余错误更容易诊断,并给你一种可复现的方式来比较服务商。规范简短而稳定,因此你今天编写的校验器在方法面扩展后依然正确。
- 把校验器加入单元测试套件,并在返回非零错误码时让构建失败
- 把结果表加入服务商评估清单
- 更换服务商或升级客户端库时重新运行结果表
- 在日志中保留原始请求体,以便重放生产错误