JSON-RPC

6 min de lecture

JSON-RPC

POST /{prefix}/{database}/jsonrpc appelle une fonction PostgreSQL et renvoie son résultat sous forme de réponse JSON-RPC 2.0. Cette page est la référence ; pour un premier exemple fonctionnel, voir le Démarrage rapide.

1. Requête

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}
ChampDescription
methodObligatoire. schema.function, 256 caractères au maximum. Les identifiants entre guillemets ("my schema"."my fn") sont autorisés. Le schéma est obligatoire : hello_world seul est rejeté avec 400. capabilities est une méthode intégrée (section 5).
paramsToute valeur JSON, transmise telle quelle à la fonction comme argument jsonb. Une valeur absente ou null devient {}. Les objets sont la convention ; les tableaux et les scalaires fonctionnent aussi.
idToute valeur JSON ; elle est recopiée dans la réponse. Si elle est absente, la réponse contient "id": null (il n’existe pas de notifications « envoyer et oublier»).
jsonrpcPar convention "2.0" ; il n’est pas validé en entrée, la réponse contient toujours "2.0".
idempotencyKeyExtension facultative (section 7). 256 caractères au maximum.

Les requêtes par lot (un tableau JSON d’appels) ne sont pas prises en charge et renvoient 400 — envoyez un appel par requête HTTP.

2. Réponse

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}

Contrairement à JSON-RPC sur une simple socket, les erreurs définissent aussi un statut HTTP significatif, ce qui permet aux clients et aux proxys de réagir sans analyser le corps. Le error.code vaut 0 sauf si un code JSON-RPC spécifique s’applique (-32601 fonction inconnue, -32001 permission refusée, -32000 requête en double). La liste complète se trouve sur la page Codes d’erreur.

3. Écrire une fonction

Une méthode est une fonction PostgreSQL qui prend exactement un argument de type jsonb et renvoie json ou jsonb. Les fonctions avec une autre signature ne sont ni listées par capabilities ni appelables.

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;
  • Valeur de retour. Le résultat JSON est envoyé comme result. Un NULL SQL (comme ci-dessus, quand aucune ligne ne correspond) devient "result": null. La fonction doit renvoyer du JSON valide : un type de retour text échoue avec 500 sauf si le texte est par hasard du JSON — utilisez to_json('hi'::text) pour un résultat de type chaîne.
  • Erreurs dans la fonction. RAISE EXCEPTION ou toute erreur SQL interrompt l’appel avec HTTP 500 et le message générique Function call failed ; le vrai message PostgreSQL n’est écrit que dans le journal du serveur, de sorte que les détails du schéma ne fuient pas. Signalez plutôt les erreurs métier attendues (introuvable, validation) dans la valeur de retour, par exemple {"ok": false, "reason": "..."}.
  • Transactions. Chaque appel s’exécute dans une transaction validée au retour de la fonction et annulée à la moindre erreur.
  • Permissions. L’appel s’exécute avec le rôle PostgreSQL authentifié, donc GRANT, REVOKE et la Row-Level Security décident de ce que l’appelant peut faire. Un rôle a besoin de USAGE sur le schéma et de EXECUTE sur la fonction. PostgreSQL accorde par défaut EXECUTE sur les nouvelles fonctions à tout le monde ; révoquez donc ce droit (ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC) et gardez les fonctions exposées dans un schéma dédié — voir Sécurité & Authentification.

4. Décrire une fonction pour les clients et les agents IA

La première ligne du commentaire de la fonction est sa description ; un objet JSON après --- PARAMS --- décrit les paramètres (properties de JSON Schema). Les deux apparaissent dans capabilities, la spécification OpenAPI et 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 commentaire dont le JSON après le marqueur est malformé est ignoré (la fonction retombe sur une description générique des paramètres) et ne casse jamais la découverte.

5. Méthodes intégrées

capabilities

Liste les fonctions que le rôle authentifié peut appeler (EXECUTE et USAGE sur le schéma) ; les schémas système et les fonctions d’extension sont masqués. Chaque entrée comporte method, description, parameters, http_method, endpoint et kind (rpc, ou file pour les fonctions de fichier).

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

La connexion pour obtenir un JWT n’est pas une méthode JSON-RPC : utilisez POST /{prefix}/{database}/token avec des identifiants HTTP Basic (section 6). L’ancienne méthode get_jwt, qui recevait le mot de passe dans params, a été supprimée et répond 404 / -32601 avec un renvoi vers le nouvel endpoint.

6. Authentification

Chaque requête nécessite un en-tête Authorization ; une requête sans cet en-tête est rejetée avec 401 avant qu’aucune connexion à la base de données ne soit ouverte.

En-têteCe qui se passe
Basic <base64(user:password)>Un pool de connexions authentifié comme cet utilisateur PostgreSQL. Le plus simple ; pas de jetons. À envoyer uniquement en HTTPS.
Bearer <jwt>Jeton issu de POST /{prefix}/{database}/token (ou de votre fournisseur d’identité). PgArachne bascule vers le rôle du jeton avec SET LOCAL ROLE ; le rôle de service a besoin de GRANT user TO pgarachne.
Bearer <api token>Jeton longue durée créé avec pgarachne.add_api_token(...), lié à un rôle. Adapté aux services.

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

Échange un identifiant et un mot de passe PostgreSQL, envoyés par authentification HTTP Basic, contre un JWT. Les identifiants voyagent dans l’en-tête Authorization — jamais dans un corps JSON — et l’endpoint a sa propre URL, de sorte qu’un reverse proxy peut appliquer des limites plus strictes aux connexions sans lire les corps de requête. Il nécessite JWT_SECRET ; sinon il renvoie 404. Chaque tentative compte dans la limite de débit de connexion et la réponse n’est jamais mise en cache.

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}'

Les tentatives échouées sont limitées en débit (HTTP 429). Tous les détails, les durées de vie des jetons et un fournisseur d’identité externe sont décrits sur la page Sécurité & Authentification.

7. Idempotence

Ajoutez "idempotencyKey": "order-42" pour rendre un appel sûr à rejouer. Le premier appel avec une clé s’exécute normalement ; une clé répétée est rejetée avant l’exécution de la fonction avec HTTP 409 et -32000 « This request has already been processed». La clé est enregistrée dans la même transaction que l’appel, donc un appel qui a échoué (et a été annulé) peut être rejoué avec la même clé. Les clés sont isolées par rôle.

  • Avec l’authentification par JWT ou jeton API, cela fonctionne immédiatement.
  • Avec l’authentification Basic, c’est l’utilisateur lui-même qui écrit la clé ; il a donc besoin de USAGE sur le schéma pgarachne et de INSERT sur pgarachne.requests (sinon l’appel échoue avec 500 Idempotency check failed).
  • Les anciennes clés ne sont pas supprimées automatiquement — planifiez pgarachne.cleanup_idempotency_keys() (voir Nettoyage des clés d’idempotence).

8. Limites

  • Corps de requête : MAX_REQUEST_BYTES (2 Mio par défaut), sinon 413.
  • method et idempotencyKey : 256 caractères chacun.
  • Échecs d’authentification : LOGIN_RATE_LIMIT / LOGIN_RATE_LIMIT_PER_IP par LOGIN_RATE_WINDOW (voir Configuration).

9. L’appeler

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}'

Depuis une page web, utilisez fetch() avec le même en-tête et le même corps ; une page complète à copier-coller (y compris SSE et téléchargement de fichiers) se trouve dans le Démarrage rapide.