Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Solución de problemas de RPC13 min de lectura

Correlación de id en JSON-RPC y orden de respuestas por lotes

Cómo JSON-RPC 2.0 correlaciona solicitudes con respuestas mediante el id, por qué las respuestas por lotes pueden llegar desordenadas y cómo construir un cliente que nunca las confunda.

TL;DR

JSON-RPC 2.0 define un contrato de correlación estricto: una solicitud que lleva un id DEBE recibir exactamente una respuesta con el mismo id, mientras que una solicitud sin id es una Notificación y NO DEBE recibir ninguna respuesta. Las respuestas por lotes pueden devolverse en cualquier orden, por lo que un cliente que asume que el orden de las respuestas coincide con el orden de las solicitudes asociará mal los resultados de forma silenciosa. El patrón correcto es asignar ids únicos, mantener un mapa de id a promesa y emparejar las respuestas por id en lugar de por posición. Este artículo explica el contrato, el caso límite id: null, las trampas de las notificaciones, la semántica de las suscripciones de Ethereum y un cliente Node.js ejecutable que puedes probar contra cualquier endpoint.

El contrato de correlación de id de JSON-RPC 2.0

La especificación JSON-RPC 2.0 define una solicitud como un objeto con jsonrpc, method, params y un id opcional. La Sección 4 establece que si se incluye id, el servidor DEBE responder con el mismo valor en la respuesta. La Sección 5 establece que una solicitud sin id es una Notificación y el servidor NO DEBE responder en absoluto. Este es todo el contrato de correlación: el id es el único campo que vincula una respuesta con su solicitud.

Debido a que el id es la única clave de correlación, debe ser único entre las solicitudes en vuelo de un cliente dado. La especificación no exige que los ids sean enteros, pero recomienda que los clientes usen enteros y eviten partes fraccionarias. En la práctica, un contador entero que aumenta monótonamente es la forma más sencilla de garantizar la unicidad sin coordinación.

La fuente autorizada de estas reglas es la especificación JSON-RPC 2.0, que es la referencia principal para la semántica del protocolo. La especificación JSON-RPC de Ethereum en ethereum.org añade nombres de métodos y formas de parámetros sobre ese contrato base sin cambiar las reglas del id.

  • Solicitud con id: exactamente una respuesta, con el mismo id.
  • Solicitud sin id: Notificación, cero respuestas.
  • Lote: un array de solicitudes; las respuestas pueden estar en cualquier orden.
  • Lote inválido: un único objeto de error, no un array.

Por qué los clientes ingenuos emparejan mal las respuestas

El error de mayor impacto en el procesamiento por lotes hecho a mano es asumir que la enésima respuesta pertenece a la enésima solicitud. La Sección 6 de la especificación permite explícitamente que el servidor devuelva las respuestas por lotes en cualquier orden, y los servidores reales aprovechan esa libertad porque procesan las solicitudes de forma concurrente. Un cliente que empareja respuestas con solicitudes por índice asociará el resultado incorrecto a la promesa incorrecta, a menudo sin lanzar un error.

El fallo es silencioso porque las respuestas JSON-RPC son estructuralmente idénticas independientemente de qué solicitud respondan. Si envías eth_blockNumber y eth_chainId en un mismo lote y el servidor los devuelve intercambiados, tu código resolverá alegremente la promesa del número de bloque con una cadena de id de cadena. Las comprobaciones de tipos pueden detectarlo, pero muchos métodos devuelven tipos que se solapan, como cadenas hexadecimales, por lo que la corrupción puede propagarse.

La solución es tratar el id como la clave de correlación y no depender nunca de la posición en el array. Esta es la misma disciplina que se describe en buenas prácticas de procesamiento por lotes JSON-RPC, que cubre cuándo procesar por lotes; este artículo cubre cómo emparejar lo que devuelve.

El patrón de mapa de id a promesa

El patrón correcto del cliente es un mapa de id a un resolver de promesa pendiente. Cuando envías una solicitud, asignas el siguiente id, almacenas el resolver bajo ese id y escribes la solicitud en el socket o en el lote. Cuando llega una respuesta, buscas el resolver por response.id, lo resuelves y eliminas la entrada. El orden nunca entra en la lógica.

Este patrón también te da un lugar natural para aplicar tiempos de espera y detectar respuestas con ids desconocidos, que normalmente indican un error del servidor o una respuesta de una conexión anterior que no se limpió. Mantener el mapa limitado a una sola conexión evita interferencias cuando te reconectas.

Para los reintentos, reutiliza el mismo id para la solicitud reintentada en lugar de inventar uno nuevo. Reutilizar el id preserva el seguimiento de idempotencia y permite que el servidor deduplique; las ventajas y desventajas se cubren en idempotencia de JSON-RPC y solicitudes duplicadas.

// Minimal id-to-promise correlation client (Node.js 18+)
const pending = new Map();
let nextId = 1;

function send(ws, method, params) {
  const id = nextId++;
  return new Promise((resolve, reject) => {
    pending.set(id, { resolve, reject });
    ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
  });
}

function onMessage(raw) {
  const msg = JSON.parse(raw);
  const entry = pending.get(msg.id);
  if (!entry) {
    console.warn('response with unknown id', msg.id);
    return;
  }
  pending.delete(msg.id);
  if (msg.error) entry.reject(new Error(JSON.stringify(msg.error)));
  else entry.resolve(msg.result);
}

Notificaciones: sin id, sin respuesta

Una solicitud sin id es una Notificación, y la especificación prohíbe que el servidor responda. Esto es útil para escrituras de tipo enviar y olvidar, como enviar una línea de log a un sumidero o enviar un evento de telemetría de mejor esfuerzo donde no necesitas confirmación. El cliente no debe asignar una promesa a una notificación, porque nunca llegará una respuesta para resolverla.

La trampa común es esperar una confirmación que la especificación prohíbe. Si envías una notificación y luego esperas una respuesta, tu cola se bloqueará hasta que se dispare un tiempo de espera, y podrías atribuir mal una respuesta posterior a la solicitud incorrecta. La regla es simple: si necesitas un resultado, incluye un id; si no, omítelo y no esperes.

Una segunda trampa es mezclar notificaciones y solicitudes en el mismo lote y luego asumir que el array de respuestas tiene la misma longitud que el array de solicitudes. No la tendrá, porque las notificaciones no producen entradas. Cuenta solo las solicitudes que llevaban un id al validar el array de respuestas.

El caso límite id: null

La Sección 5 de la especificación reserva id: null para respuestas a solicitudes cuyo id no se pudo detectar, como un error de análisis o una solicitud inválida. En esos casos el servidor no puede repetir un id porque nunca leyó uno correctamente, por lo que devuelve null. Esta es una señal a nivel de protocolo, no un valor de aplicación.

No debes usar null como un id de aplicación normal. Si tu cliente envía id: null, no puedes distinguir una respuesta legítima de una respuesta de error generada porque el servidor no pudo analizar tu solicitud. Reserva null para el servidor y usa enteros positivos en el cliente.

Un ejemplo práctico: si envías un cuerpo JSON mal formado, el servidor devuelve un único objeto con id: null y un código de error -32700 (Error de análisis). Tu cliente debe tratar cualquier respuesta con id: null como un error de protocolo, no como la respuesta a una solicitud que enviaste.

// Server response to a malformed request body
{
  "jsonrpc": "2.0",
  "id": null,
  "error": { "code": -32700, "message": "Parse error" }
}

eth_subscribe de Ethereum y notificaciones de suscripción

El flujo de eth_subscribe de Ethereum es diferente de un par normal de solicitud/respuesta. La llamada subscribe en sí es una solicitud normal con un id y recibe una respuesta normal que contiene una cadena de id de suscripción. Después de eso, el servidor envía notificaciones eth_subscription que llevan el id de suscripción dentro de params.subscription, no como un id de solicitud de nivel superior.

Esto significa que tu lógica de correlación necesita dos capas. La primera capa empareja la respuesta de subscribe por el id de solicitud. La segunda capa enruta los mensajes eth_subscription entrantes por params.subscription al manejador registrado para esa suscripción. Tratar el id de suscripción como un id de solicitud no funcionará porque estas son notificaciones, no respuestas.

La distinción importa cuando combinas suscripciones con llamadas ordinarias en un mismo socket. La comparación en logs de eth_subscribe vs sondeo por WebSocket explica cuándo el modelo push merece la capa de enrutamiento adicional.

// Subscribe response (normal id correlation)
{ "jsonrpc": "2.0", "id": 1, "result": "0x9cef478923ff08bf67fde6c64013158d" }

// Pushed notification (route by params.subscription, not id)
{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0x9cef478923ff08bf67fde6c64013158d",
    "result": { "number": "0x10d4f" }
  }
}

Ejemplo de lote ejecutable con reasociación fuera de orden

El ejemplo siguiente envía un lote de tres elementos con ids 1, 2 y 3, y luego procesa deliberadamente un array de respuestas mezclado para demostrar que la correlación es por id, no por posición. Imprime cada solicitud junto con su respuesta emparejada para que puedas ver la asociación explícitamente.

Ejecútalo contra cualquier endpoint JSON-RPC que admita HTTP POST. Reemplaza la URL con tu propio endpoint; la guía de endpoints RPC explica cómo obtener uno. El código usa solo la biblioteca estándar de Node.js.

// node batch-correlate.mjs
const URL = process.env.RPC_URL || 'https://your-endpoint.example';

const requests = [
  { jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] },
  { jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
  { jsonrpc: '2.0', id: 3, method: 'net_version', params: [] }
];

const res = await fetch(URL, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(requests)
});
const responses = await res.json();

// Simulate a server that returns responses out of order.
const shuffled = [...responses].reverse();

const byId = new Map(requests.map(r => [r.id, r]));
for (const resp of shuffled) {
  const req = byId.get(resp.id);
  console.log('request', req.method, 'id', req.id, '->', JSON.stringify(resp.result ?? resp.error));
}

Construir una tabla de correlación contra tu propio endpoint

Para producir evidencia reproducible de que tu endpoint respeta el contrato del id, ejecuta el ejemplo de lote anterior y registra cada par solicitud/respuesta en una tabla. Vuelve a ejecutarlo cuando quieras; la tabla es tu propia medición, no una afirmación de un proveedor. Esto separa el comportamiento documentado del protocolo del comportamiento observado del proveedor, que puede variar.

Rellena una fila por solicitud. La columna matched debe ser yes solo cuando el id de la respuesta sea igual al id de la solicitud. La columna de latencia es el tiempo de reloj de pared desde el envío hasta la recepción de ese id. La columna de error captura cualquier objeto de error JSON-RPC. Si alguna fila muestra matched = no, tu cliente o el endpoint está violando el contrato.

  • Id de solicitud: el entero que asignaste.
  • Método: el nombre del método JSON-RPC.
  • Id de respuesta: el id devuelto por el servidor.
  • Matched: yes si el id de la respuesta es igual al id de la solicitud.
  • Latencia ms: tiempo medido de envío a recepción.
  • Error: cualquier objeto de error devuelto, o ninguno.

Multiplexación de conexiones, HTTP/2 y reintentos

Cuando multiplexas muchas llamadas sobre una sola conexión, las respuestas pueden llegar genuinamente desordenadas. HTTP/2 intercala flujos en una única conexión TCP, y los frames de WebSocket llegan en el orden en que el servidor los escribe, que no tiene por qué coincidir con el orden en que enviaste las solicitudes. El mapa de ids maneja ambos casos sin lógica especial.

Para los reintentos, reutiliza el id de la solicitud original. Inventar un nuevo id por reintento rompe el seguimiento de idempotencia porque el servidor ve dos solicitudes distintas. Los detalles del comportamiento de reintento seguro están en idempotencia de JSON-RPC y solicitudes duplicadas.

La reutilización de conexiones y la configuración de keep-alive afectan a cuántas solicitudes en vuelo comparten un socket, lo que a su vez afecta a cuánta libertad de orden tiene el servidor. Consulta reutilización de conexiones RPC y keep-alive HTTP/2 para el lado del transporte de este panorama.

Lista de verificación de solución de problemas para fallos de id y orden

Usa esta lista de verificación cuando las respuestas parezcan mal emparejadas o falten. Cada elemento se corresponde con una cláusula específica de la especificación, por lo que puedes decidir si el fallo está en tu cliente o en el endpoint.

Si una respuesta lleva un id que nunca enviaste, trátala como un error de protocolo y regístrala. Si un elemento del lote no tiene respuesta, comprueba si omitiste accidentalmente su id, convirtiéndolo en una notificación. Si una notificación parece bloquear una cola, estás esperando una respuesta que la especificación prohíbe. Si enviaste ids duplicados en un mismo lote, la especificación lo trata como un error del cliente, así que corrige el asignador de ids.

  • Respuesta con id inesperado: regístrala y descártala; comprueba si hay estado de conexión obsoleto.
  • Falta la respuesta de un elemento del lote: verifica que la solicitud llevaba un id.
  • Notificación que bloquea una cola: elimina la promesa; las notificaciones nunca se resuelven.
  • Ids duplicados en un mismo lote: error del cliente; impón un contador único.
  • Un único objeto de error en lugar de un array: el lote en sí era inválido.

Limitaciones y ventajas y desventajas

La especificación no limita el tamaño del lote, por lo que un lote muy grande puede ser rechazado por un proveedor por razones ajenas al protocolo. Tampoco exige una única respuesta por lote cuando el lote en sí es inválido: el servidor devuelve un único objeto de error, no un array. Tu cliente debe manejar tanto la forma de array como la forma de objeto único.

El comportamiento del proveedor varía. Algunos endpoints pueden devolver las respuestas en el orden de las solicitudes como detalle de implementación, pero no debes depender de ello porque la especificación permite cualquier orden. El comportamiento documentado es que el orden no está garantizado; cualquier cosa más estricta es específica del proveedor y puede cambiar.

Por último, la correlación por id no resuelve la idempotencia a nivel de aplicación ni el orden de los efectos secundarios. Dos solicitudes con ids diferentes aún pueden aplicarse en un orden que no pretendías. Para los métodos de escritura, combina la correlación de id con la guía de idempotencia enlazada arriba.

Próximos pasos para clientes de producción

Empieza por reemplazar cualquier manejo de respuestas basado en índices por un mapa de ids, y luego añade la prueba de la tabla de correlación a tu CI para detectar regresiones. Una vez que la correlación sea sólida, ajusta los tamaños de lote y la reutilización de conexiones usando la guía de buenas prácticas de procesamiento por lotes JSON-RPC.

Si estás eligiendo un endpoint, revisa precios de RPC y la descripción general del servicio de API, y explora el centro de aprendizaje de OnFinality para temas de integración relacionados. Para métodos específicos de Ethereum, consulta la página de la red Ethereum.

Mantén la especificación JSON-RPC 2.0 abierta como tu referencia principal. Cuando el comportamiento de un proveedor se desvíe de ella, trata la desviación como documentada pero variable y pruébala tú mismo con el método de la tabla anterior.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar