Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
RPC 故障排查阅读约 12 分钟

JSON-RPC 错误对象:解码 code、message、data

了解 JSON-RPC 2.0 错误对象的确切约定,以及如何按 code、message 和 data 对任何 RPC 失败进行分类。

TL;DR

JSON-RPC 2.0 错误对象恰好有三个成员:code(整数,必需)、message(字符串,必需)和 data(可选,无约束)。规范预定义了从 -32700 到 -32603 的五个错误码,并将 -32000 到 -32768 保留给实现定义的服务器错误。正确的客户端会区分错误响应、成功的 null 结果和传输失败,将错误码分类为重试、修复或中止操作,并且从不解析人类可读的 message。本文展示了该约定、一个可运行的 Node.js 解码器、批量 id 映射,以及一张你针对自己端点填写的结果表。

JSON-RPC 2.0 错误对象约定

JSON-RPC 2.0 规范在第 5.1 节中定义了错误对象,包含三个成员:codemessagedata。只有 codemessage 是必需的;data 是可选的,规范有意不约束其类型和含义。响应对象必须包含 resulterror,绝不能同时包含两者,并且 id 成员必须与请求匹配。

由于 resulterror 互斥,error 键的存在就是失败的规范性信号。code 成员是整数,是客户端唯一可以分支判断的稳定判别符。message 成员是简短的人类可读字符串,规范明确指出它面向开发者而非最终用户。

该约定与传输无关。无论你是通过 OnFinality 的以太坊网络端点还是任何其他 JSON-RPC 服务调用以太坊节点,相同的三成员结构都适用。预定义错误码之外的提供商特定行为按提供商记录,并因提供商而异。

  • code:整数,必需,稳定的机器可读判别符。
  • message:字符串,必需,人类可读,对逻辑非规范性。
  • data:可选,无约束,用于结构化细节的逃生舱。
  • resulterror 在单个响应对象中互斥。

预定义错误码和保留的服务器范围

JSON-RPC 2.0 规范第 5.1.1 节预定义了五个错误码。-32700 是解析错误,表示收到了无效 JSON。-32600 是无效请求,表示 JSON 有效但不是有效的请求对象。-32601 是方法未找到。-32602 是无效参数。-32603 是内部错误。

规范还保留了 -32000-32768 范围用于实现定义的服务器错误。这是关键警告:该范围内的错误码没有跨提供商含义。节点可能将 -32000 重用于许多不同情况,提供商也可能在预定义集合之外添加自己的错误码。因此,跨提供商的错误码比较仅对五个预定义错误码可靠。

以太坊 JSON-RPC 规范在此基础上构建,并记录了以太坊特定的错误行为,包括 revert 数据的位置。将以太坊规范视为以太坊语义的权威,将 JSON-RPC 2.0 规范视为信封的权威。

  • -32700 解析错误:无效 JSON。
  • -32600 无效请求:有效 JSON,但请求对象无效。
  • -32601 方法未找到。
  • -32602 无效参数。
  • -32603 内部错误。
  • -32000-32768:保留给实现定义的服务器错误。

区分错误响应、null 结果和传输失败

最常见的客户端 bug 是将 result: nullerror: {...} 视为同一种失败。它们不是。成功的调用可能合法地返回 null——例如,查询尚不存在的区块高度——那是成功,不是错误。传输失败是完全不同的层:HTTP 请求可能失败、超时,或在任何 JSON-RPC 信封存在之前返回非 JSON 正文。

正确的类型守卫按顺序检查三件事:传输是否成功,正文是否解析为 JSON,解析后的对象是否包含 error 成员。只有第三个条件才是 JSON-RPC 错误。这种顺序可防止你将网络中断误分类为协议错误,或将 null 结果误分类为失败。

当你阅读 revert 原因和自定义错误时,同样的纪律适用:revert 细节位于错误对象的 data 中,但只有在你确认自己面对的是错误对象之后。

function classifyResponse(httpOk, bodyText) {
  if (!httpOk) return { kind: 'transport', retryable: true };
  let parsed;
  try {
    parsed = JSON.parse(bodyText);
  } catch (e) {
    return { kind: 'transport', retryable: true, reason: 'non-json body' };
  }
  if (parsed && typeof parsed === 'object' && 'error' in parsed) {
    return { kind: 'jsonrpc-error', error: parsed.error, id: parsed.id };
  }
  if (parsed && typeof parsed === 'object' && 'result' in parsed) {
    return { kind: 'success', result: parsed.result, id: parsed.id };
  }
  return { kind: 'malformed', retryable: false };
}

将错误码范围映射到重试、修复或中止操作

一旦确认了错误对象,code 就决定操作。解析错误(-32700)是客户端 bug:你的序列化器产生了无效 JSON,重试相同负载会以相同方式失败。无效请求(-32600)也是请求信封中的客户端 bug。两者都不可重试。

-32601 方法未找到和 -32602 无效参数几乎总是错误的方法名或格式错误的参数数组。这些通过代码更改修复,而不是重试。-32603 内部错误是模糊的那个:它可能是暂时的,因此重试一次然后上报。-32000 范围内的错误码是节点或链特定的,必须按链处理,因为相同的数字错误码在不同提供商上可能含义不同。

这种分类是通用层,位于链特定解码器(如 Solana simulateTransaction 错误解码)之下。通用层决定是否重试;链特定层决定失败的含义。

  • -32700-32600:客户端 bug,绝不重试,修复负载。
  • -32601-32602:错误的方法或参数,在代码中修复,不要重试。
  • -32603:重试一次,然后上报给调用方。
  • -32000 范围:链或节点特定,按链处理。
  • 未知错误码:带完整上下文上报,而不是猜测。

为什么绝不能解析 message,而 code 是唯一稳定的键

message 成员是人类文本。提供商会本地化、包装或重写它,规范不约束其措辞。任何对 message 进行字符串匹配的客户端都会与提供商的措辞耦合,并在措辞更改时中断。code 成员是唯一稳定的判别符。

需要注意的是,提供商会将 -32000 重用于许多不同情况,并可能在规范之外添加自己的错误码。这意味着 code 在提供商的文档化集合内是稳定的,但不一定跨提供商可比。为人类记录 message,为逻辑分支使用 code,并为 -32000 范围保留每个提供商的映射表。

如果你需要跨提供商比较行为,请将自动化逻辑限制在五个预定义错误码,并将其他一切视为提供商特定。这与你在推理 JSON-RPC 幂等性和重复请求安全时应用的关注点分离相同:协议保证信封,而不是供应商的语义。

data 字段作为结构化细节的逃生舱

data 成员是实现放置不适合 codemessage 的结构化细节的地方。对于以太坊,revert 字符串或 ABI 编码的自定义错误通常放在那里。规范不要求 data 存在,因此客户端绝不能假设它存在。

实际后果是你的解码器应将 data 视为可选,并在使用前验证其形状。如果你期望 ABI 编码的自定义错误,在尝试解码之前检查 data 是足够长度的十六进制字符串。如果它不存在,则回退到 codemessage 进行分类。

当你使用 eth_call 状态覆盖模拟调用时,相同的 data 字段携带 revert 细节,因此解码路径在实时调用和模拟之间共享。

function extractRevertData(error) {
  if (!error || typeof error !== 'object') return null;
  const d = error.data;
  if (typeof d === 'string' && /^0x[0-9a-fA-F]*$/.test(d)) return d;
  if (d && typeof d === 'object' && typeof d.data === 'string') return d.data;
  return null;
}

可运行的 Node.js 示例:原始 fetch 与包装库错误

库会隐藏原始错误对象。下面的示例发出原始 fetch 调用,以便你可以看到确切的 JSON-RPC 错误信封,然后展示包装库错误通常如何嵌套相同字段。针对你自己的端点运行它,以观察提供商返回的真实形状。

将端点 URL 替换为你自己的。目标不是产生特定结果,而是揭示传输响应与库抽象之间的差异,以便你决定错误处理应位于哪一层。

const endpoint = process.env.RPC_URL || 'https://your-endpoint.example';

async function rawCall() {
  const res = await fetch(endpoint, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'eth_getBalance',
      params: ['0x0000000000000000000000000000000000000000', 'latest']
    })
  });
  const text = await res.text();
  console.log('http status:', res.status);
  console.log('raw body:', text);
  try {
    const parsed = JSON.parse(text);
    if (parsed.error) {
      console.log('code:', parsed.error.code);
      console.log('message:', parsed.error.message);
      console.log('data present:', 'data' in parsed.error);
    }
  } catch (e) {
    console.log('body was not JSON');
  }
}

rawCall().catch((e) => console.error('transport failure:', e.message));

批量响应和按 id 将错误映射回请求

批量请求发送请求对象数组并接收响应对象数组。某些条目可能携带 result,其他条目可能携带 error,并且每个条目携带自己的 id。数组顺序不保证与请求顺序匹配,因此你必须按 id 而不是按位置映射。

在发送前构建从 id 到请求的映射,然后遍历响应数组,将每个结果或错误附加到其原始请求。这是当批量部分成功时知道哪个调用失败的唯一可靠方法。

如果你正在批量写入,在重试失败条目之前,请查看 JSON-RPC 幂等性和重复请求安全,因为重试部分应用的批量可能会重复效果。

function mapBatch(requests, responses) {
  const byId = new Map(requests.map((r) => [r.id, r]));
  return responses.map((resp) => {
    const req = byId.get(resp.id);
    if (resp.error) {
      return { id: resp.id, method: req && req.method, status: 'error', error: resp.error };
    }
    return { id: resp.id, method: req && req.method, status: 'ok', result: resp.result };
  });
}

结果表:针对你自己的端点测量错误行为

下表是一个可复现的测量模板。通过将每个请求发送到你自己的端点并记录返回内容来填写它。不要依赖本文中的数字;重点是观察你的提供商的实际行为。

每个请求至少运行两次,以区分确定性错误和暂时性错误。记录 data 是否存在,因为这决定你是否可以解码结构化细节。然后使用上面的范围分配分类和操作。

  • 请求:你发送的确切 JSON-RPC 方法和参数。
  • Code:来自 error.code 的整数。
  • Message:来自 error.message 的字符串,逐字记录。
  • Data 存在?:是或否,如果存在则记录其类型。
  • 分类:解析、无效请求、方法、参数、内部或服务器范围。
  • 操作:重试、修复或中止,并附原因。

通用错误解码的局限性和权衡

规范有意将 -32000 范围留给实现,因此跨提供商错误码比较仅对预定义集合可靠。任何假设特定 -32000 含义的逻辑都会与提供商耦合,并可能在你切换端点或提供商更改映射时中断。

绝不能假设结构化 data 存在。要求 data 的解码器会在省略它的提供商上失败。安全模式是在 data 存在时尝试结构化解码,在不存在时回退到 codemessage

通用处理和链特定处理之间也存在权衡。通用分类决定重试还是修复;链特定解码器决定含义。将这些层分开使两者都更易于测试,但这意味着你维护两个映射而不是一个。

反复出现的 RPC 故障排查清单

当故障反复出现时,按顺序逐层处理。首先确认传输成功且正文解析为 JSON。然后确认你看到的是 error 成员而不是 null 结果。然后读取 code 并分类。只有在这之后才应检查 data

如果错误码是 -32601-32602,在更改其他任何内容之前,对照 以太坊 JSON-RPC 规范检查方法名和参数数组。如果错误码在 -32000 范围内,请查阅提供商的文档,因为含义是提供商特定的。

对于不是协议错误的端点选择和连接问题,RPC 端点指南涵盖了如何选择和验证端点。关于重试的容量规划,请参阅 RPC 定价API 服务概述。

  • 传输正常?正文解析为 JSON?
  • 是否有 error 成员,还是这是 null 结果?
  • code 是什么,它落在哪个范围?
  • data 是否存在,其形状是否符合你的预期?
  • 错误码是否是提供商特定的,需要按提供商处理?

下一步:构建可复用的错误解码器

将上述部分整合为一个小模块:一个类型守卫,区分传输、成功和错误;一个分类器,将错误码映射到重试、修复或中止;以及一个可选的 data 提取器用于链特定解码。将提供商特定的 -32000 映射放在配置中而不是代码中,这样你无需发布即可更新它。

然后将该模块接入你的调用点,并记录完整的错误对象,包括 codemessage 以及 data 是否存在。该日志使结果表随时间可复现。

关于 RPC 使用模式的更广泛背景,请从 OnFinality Learn 中心以太坊网络页面开始。目标是构建一个能在提供商变更后存活的解码器,因为它基于协议约定而不是供应商的措辞进行分支。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始