Ajouter l'idempotence à nos requêtes
Ce cours fait partiellement suite à celui de React.
C'est quoi
L'idempotence ça sert à faire en sorte que deux requêtes identiques ne s'appliquent pas toutes les deux. Par exemple, au moment d'appuyer sur un bouton "créer", si un utilisateur clique deux fois, la requête est lancée deux fois, et l'entité sera créée deux fois. On veut éviter ça.
Mise en place
Pour mettre ça en place, on va créer dans la bdd une table "IdempotencyKey" qui va persister certaines infos comme ça les requêtes envoyées plusieurs fois ne feront rien. Voici un exemple de cette table :
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
export const idempotencyKey = sqliteTable("idempotencyKey", {
id: text().primaryKey(),
key: text().notNull().unique(),
userId: text(),
method: text(),
path: text(),
requestHash: text(),
status: int(),
})
Création d'un "hasheur" de requête
On va créer un fichier "hash.js" qui va nous permettre de hasher les requêtes.
import crypto from 'node:crypto';
export function stableHash(obj) {
const s = JSON.stringify(obj, Object.keys(obj).sort());
return crypto.createHash('sha256').update(s).digest('hex');
}
Ça va servir à vérifier que si deux requêtes on la même "key" mais pas le même body, ça ne soit pas la même requête.
Middleware idempotence
On va créer un middleware qui va nous permettre de gérer l'idempotence de nos requêtes. Il nous suffira de l'ajouter à toutes nos routes qui auront besoin d'être idempotent.
import { stableHash } from '../lib/hash.js';
import { db } from '../db/clientDb.js';
import { idempotencyKey } from '../db/schema.js';
import { createMiddleware } from 'hono/factory';
export const withIdempotency = createMiddleware(async (c, next) => {
const key = c.req.header('idempotency-key');
if (!key) {
return c.json(
{ error: { code: 'IDEMPOTENCY_KEY_REQUIRED' } },
400
);
}
const method = c.req.method.toUpperCase();
const url = new URL(c.req.url);
const path = url.pathname;
const userId = c.get('user')?.id ?? null;
let body = null;
if (!['GET', 'HEAD'].includes(method)) {
body = await c.req.json().catch(() => ({}));
}
const reqHash = body ? stableHash(body) : undefined;
// Try to insert a new key; if exists, return stored response
const now = new Date();
const expiresAt = new Date(now.getTime() + ttlSec * 1000);
try {
await db.insert(idempotencyKey).values({
key,
userId,
method,
path,
requestHash: reqHash,
expiresAt,
});
} catch (e) {
if (e) {
return c.json(
{ error: { code: 'IDEMPOTENCY_BAD_REQUEST' } },
400
);
}
}
await next();
}
Ajouter le middleware à nos routes
import { withIdempotency } from './middlewares/idempotency.js';
export const app = new Hono();
app.post('/tasks', withIdempotency, (c) => {
// TODO: Ajouter la tâche à la base de données
});
Setup front-end
Il suffira ici simplement de créer une UUID random pour chaque requête. Exemple :
await fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({ title: 'Tâche 1', description: 'Description de la tâche 1' }),
});