JSON-RPC

6 min de lectura

JSON-RPC

POST /{prefix}/{database}/jsonrpc llama a una función de PostgreSQL y devuelve su resultado como una respuesta JSON-RPC 2.0. Esta página es la referencia; para un primer ejemplo funcional consulta Inicio rápido.

1. Petición

POST /db/my_database/jsonrpc
Authorization: Basic ...            (or Bearer <jwt / api token>)
Content-Type: application/json

{"jsonrpc": "2.0", "method": "api.hello_world", "params": {"name": "Alice"}, "id": 1}
CampoDescripción
methodObligatorio. schema.function, como máximo 256 caracteres. Se permiten identificadores entre comillas ("my schema"."my fn"). El esquema es obligatorio: hello_world solo se rechaza con 400. capabilities es un método integrado (sección 5).
paramsCualquier valor JSON, pasado sin cambios a la función como su argumento jsonb. Si falta o es null, se convierte en {}. Por convención son objetos; los arrays y escalares también funcionan.
idCualquier valor JSON; se copia en la respuesta. Si falta, la respuesta contiene "id": null (no hay notificaciones de tipo «dispara y olvida»).
jsonrpcPor convención "2.0"; no se valida en la entrada, la respuesta siempre lleva "2.0".
idempotencyKeyExtensión opcional (sección 7). Como máximo 256 caracteres.

Las peticiones por lotes (un array JSON de llamadas) no están soportadas y devuelven 400 — envía una llamada por petición HTTP.

2. Respuesta

HTTP 200  {"jsonrpc":"2.0","result":{"message":"Hello, Alice!"},"id":1}

HTTP 404  {"jsonrpc":"2.0","error":{"code":-32601,"message":"Function does not exist"},"id":1}

A diferencia de JSON-RPC puro sobre un socket, los errores también establecen un estado HTTP significativo, de modo que clientes y proxies pueden reaccionar sin analizar el cuerpo. El error.code es 0 salvo que se aplique un código JSON-RPC específico (-32601 función desconocida, -32001 permiso denegado, -32000 petición duplicada). La lista completa está en la página Códigos de Error.

3. Escribir una función

Un método es una función de PostgreSQL que recibe exactamente un argumento de tipo jsonb y devuelve json o jsonb. Las funciones con otras firmas ni aparecen en capabilities ni se pueden llamar.

CREATE OR REPLACE FUNCTION api.find_user(payload jsonb DEFAULT '{}'::jsonb)
RETURNS json LANGUAGE sql STABLE AS $$
    SELECT row_to_json(u) FROM (
        SELECT id, name FROM app.users WHERE id = (payload->>'id')::int
    ) u;
$$;

GRANT USAGE ON SCHEMA api TO web_user;
GRANT EXECUTE ON FUNCTION api.find_user(jsonb) TO web_user;
  • Valor de retorno. El resultado JSON se envía como result. Un NULL de SQL (como arriba, cuando ninguna fila coincide) se convierte en "result": null. La función debe devolver JSON válido: un tipo de retorno text falla con 500 salvo que el texto sea JSON — usa to_json('hi'::text) para un resultado de tipo cadena.
  • Errores dentro de la función. RAISE EXCEPTION o cualquier error SQL aborta la llamada con HTTP 500 y el mensaje genérico Function call failed; el mensaje real de PostgreSQL solo se escribe en el log del servidor, así que no se filtran detalles del esquema. Informa de los errores de negocio esperados (no encontrado, validación) en el valor de retorno, por ejemplo {"ok": false, "reason": "..."}.
  • Transacciones. Cada llamada se ejecuta en una transacción que se confirma cuando la función retorna y se revierte ante cualquier error.
  • Permisos. La llamada se ejecuta como el rol de PostgreSQL autenticado, así que GRANT, REVOKE y Row-Level Security deciden qué puede hacer quien llama. Un rol necesita USAGE en el esquema y EXECUTE en la función. PostgreSQL concede EXECUTE sobre las funciones nuevas a todos por defecto, así que revócalo (ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC) y mantén las funciones expuestas en un esquema dedicado — consulta Seguridad y Autenticación.

4. Describir una función para clientes y agentes de IA

La primera línea del comentario de la función es su descripción; un objeto JSON después de --- PARAMS --- describe los parámetros (properties de JSON Schema). Ambos aparecen en capabilities, en la especificación OpenAPI y en MCP tools/list:

COMMENT ON FUNCTION api.find_user(jsonb) IS 'Looks up a user by id.
--- PARAMS ---
{"id": {"type": "integer", "description": "User id"}}';

Un comentario con JSON mal formado después del marcador se ignora (la función recurre a una descripción genérica de parámetros) y nunca rompe el descubrimiento.

5. Métodos integrados

capabilities

Lista las funciones que el rol autenticado puede llamar (EXECUTE y USAGE del esquema); los esquemas del sistema y las funciones de extensiones están ocultos. Cada entrada tiene method, description, parameters, http_method, endpoint y kind (rpc, o file para funciones de archivo).

{"jsonrpc":"2.0","method":"capabilities","id":1}

Iniciar sesión para obtener un JWT no es un método JSON-RPC: usa POST /{prefix}/{database}/token con credenciales HTTP Basic (sección 6). El antiguo método get_jwt, que recibía la contraseña en params, fue eliminado y responde 404 / -32601 con una indicación del nuevo endpoint.

6. Autenticación

Toda petición necesita una cabecera Authorization; una petición sin ella se rechaza con 401 antes de abrir cualquier conexión a la base de datos.

CabeceraQué ocurre
Basic <base64(user:password)>Un pool de conexiones autenticado como ese usuario de PostgreSQL. Lo más simple; sin tokens. Envíalo solo por HTTPS.
Bearer <jwt>Token de POST /{prefix}/{database}/token (o de tu propio proveedor de identidad). PgArachne cambia al rol del token con SET LOCAL ROLE; el rol de servicio necesita GRANT user TO pgarachne.
Bearer <api token>Token de larga duración creado con pgarachne.add_api_token(...), vinculado a un rol. Adecuado para servicios.

Obtener un JWT: POST /{prefix}/{database}/token

Intercambia un login y contraseña de PostgreSQL, enviados con autenticación HTTP Basic, por un JWT. Las credenciales viajan en la cabecera Authorization — nunca en un cuerpo JSON — y el endpoint tiene su propia URL, de modo que un proxy inverso puede aplicar límites más estrictos a los inicios de sesión sin leer cuerpos de petición. Requiere JWT_SECRET; en caso contrario devuelve 404. Cada intento cuenta para el límite de frecuencia de inicio de sesión y la respuesta nunca se puede almacenar en caché.

curl -X POST http://localhost:8080/db/my_database/token -u web_user:password
→ {"token":"eyJhbGciOi...","token_type":"Bearer","expires_in":28800}

curl http://localhost:8080/db/my_database/jsonrpc -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"capabilities","id":1}'

Los intentos fallidos tienen límite de frecuencia (HTTP 429). Todos los detalles, la duración de los tokens y un proveedor de identidad externo están en la página Seguridad y Autenticación.

7. Idempotencia

Añade "idempotencyKey": "order-42" para que una llamada se pueda reintentar con seguridad. La primera llamada con una clave se ejecuta con normalidad; una clave repetida se rechaza antes de ejecutar la función con HTTP 409 y -32000 «This request has already been processed». La clave se guarda en la misma transacción que la llamada, así que una llamada que falló (y se revirtió) puede reintentarse con la misma clave. Las claves están en un espacio de nombres por rol.

  • Con autenticación por JWT o token de API funciona sin configuración adicional.
  • Con autenticación Basic el propio usuario escribe la clave, así que necesita USAGE en el esquema pgarachne e INSERT en pgarachne.requests (de lo contrario la llamada falla con 500 Idempotency check failed).
  • Las claves antiguas no se eliminan automáticamente — programa pgarachne.cleanup_idempotency_keys() (consulta Limpieza de Claves de Idempotencia).

8. Límites

  • Cuerpo de la petición: MAX_REQUEST_BYTES (por defecto 2 MiB); si se supera, 413.
  • method e idempotencyKey: 256 caracteres cada uno.
  • Fallos de autenticación: LOGIN_RATE_LIMIT / LOGIN_RATE_LIMIT_PER_IP por LOGIN_RATE_WINDOW (consulta Configuración).

9. Llamarlo

curl -X POST http://localhost:8080/db/my_database/jsonrpc \
  -u web_user:password -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"api.find_user","params":{"id":1},"id":1}'

Desde una página web usa fetch() con la misma cabecera y cuerpo; una página completa para copiar y pegar (incluidos SSE y descarga de archivos) está en Inicio rápido.