Qu’est-ce qu’un Webhook ?
Les Webhooks sont un moyen innovant en programmation web pour notifier à des applications externes qu’un changement ou qu’une évolution de vos données a été réalisée au sein de votre propre application.
Les Webhooks ne remplace pas une API. C’est un moyen complémentaire à une API pour récupérer et être au courant des nouvelles données.
En clair, un Webhook vous évitera de devoir interroger une API à intervalle régulier pour savoir si quelque chose à changer puisque c’est lui qui vous le notifiera : ceux qui rend votre processus de récupération de données plus performants et plus efficaces.
Concrètement, les Webhooks vont se déclencher après qu’une requête POST, PUT, PATCH ou DELETE ait réussi au niveau de votre API.
Le résultat de ces requêtes est généralement envoyé directement aux applications ayant souscrit à votre système de Webhooks. Il est tout à fait possible de reformater la réponse avant de la renvoyer.
Le Webhook doit envoyer ses données aux applications externes en méthode POST.
Schéma pour se représenter le fonctionnement d’un Webhook :
Gérer techniquement un système de production de Webhook au sein de son API
Il est existe beaucoup de contenu sur la façon de souscrire à des webhooks pour recevoir des données et les traiter comme ceux de Stripe, GitHub, etc…
Mais il existe peu de contenu sur la façon d’en produire pour d’autres.
Pour produire des Webhooks sur son API, il faut pouvoir créer un système qui :
- s’exécutera après que les routes de votre API aient terminé et réussi leur traitement
- s’exécutera uniquement sur des routes POST, PUT, PATCH ou DELETE que vous souhaitez ouvrir
- permettra d’identifier le type d’événement par rapport au contenu des données de votre Webhook (en ajoutant une en-tête HTTP dans le Webhook)
- permettra de retenter « X » fois votre webhook si l’application externe vous a renvoyé un statut code différent de 200
- ne devra pas bloquer les réponses ou les autres traitements de votre API (il devra fonctionner de manière asynchrone, autonome et sans observer les retours d’états.
- permettra de manager la liste des applications souscrivant au webhook
- assurera un moyen de sécuriser la transaction pour l’application souscrivant à votre Webhook
Comment ça se passe techniquement sur un environnement Node.js ?
Un package NPM node-webhooks 🔗 permet de créer un système de production de Webhooks sur Node.js (donc sur Express ou NestJS).
Le principe est relativement simple :
- on instancie le package avec une liste d’URL d’application qui recevront vos données en méthode POST puis on lancer la méthode .trigger() du package pour déclencher la production d’un Webhook dans votre système de Webhooks.
- Il sera ensuite possible possible de créer un système de « retries » grâce à la méthode .getEmitter() ce qui vous donnera la possibilité de gérer les événements success ou failure de votre système de Webhooks. L’événement failure sera intéressant pour gérer un système de « retries ». Exemple de « retries » possible :
const emitter = webHooks.getEmitter();
let retries = 3;
emitter.on("*.failure", (shortname, statusCode, body) => {
console.error(
`Error on trigger webHook ${shortname} with status code ${statusCode} and body ${body} - Remaining number of retries the webhooks : ${retries}`
);
const retryWebhooks = setTimeout(() => {
retries--;
// re-lauch your webhooks system
}, 30000);
if (retries === 0) {
clearTimeout(retryWebhooks);
}
});
- Il faudra ensuite produire un système de sécurité de votre transaction. Vous pouvez spécifier que les applications externes qui souscrivent à un Webhook requièrent de demander une authentification basique (
BasicAuth
). Mais il existe une autre alternative que je vous conseille : créer une signature pour votre Webhook. Le meilleur moyen est de créer un hash du contenu du body avec un secret et de placer la signature dans une en-tête HTTP. Ce qui donnera cette formule :hash_hmac('sha256', body, secret)
. Il suffit ensuite aux applications externes de contrôler la signature en la recréant de leur côté (avec le même secret). Exemple aveccrypto
:crypto.createHmac('sha256', 'my_secret').update(JSON.stringify(req.body)).digest('base64')
Le moyen le plus simple et le plus efficace est de créer un middleware sur votre API qui s’occupera d’analyser la réponse et suivant le statut de la réponse appellera votre système de Webhooks.
Un bon vieux app.use()
fera l’affaire sur Express tandis qu’un interceptor custom sur NestJS sera plus efficace.
Exemple de code pour intégrer le package sous NestJS et produire des Webhooks
Dans mon exemple, je me récupère un decorator custom appelé Abilities (qui contient le type de droit pour utiliser la route) sur la route utilisée pour construire mon MyAPI-Hook
qui contiendra le type d’événement pour les applications souscrivant au Webhook savent le type de données et ce que cela concerne.
SECRETS_WEBHOOKS = d6b88fed70be13e62b8e31147acda29ef91f651f;
LIST_URL_FOR_SEND_WEBHOOKS = { shortname1: ["http://localhost:3000/webhooks"] };
MS_RETRY_DELAY_WEBHOOKS = 5000;
NUMBER_OF_RETRY_WEBHOOKS = 5;
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { Reflector } from "@nestjs/core";
import * as WebHooks from "node-webhooks";
import * as crypto from "crypto";
@Injectable()
export class WebhooksProducerInterceptor implements NestInterceptor {
private produceWebhook(webHooks, data, apiHook) {
webHooks.trigger("myapi-teams", data, {
"MyAPI-Signature": crypto
.createHmac("sha256", process.env.SECRETS_WEBHOOKS)
.update(JSON.stringify(data))
.digest("base64"),
"MyAPI-Hook": `${apiHook}`,
});
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const webHooks = new WebHooks({
db: JSON.parse(process.env.LIST_URL_FOR_SEND_WEBHOOKS),
});
const getReflector = new Reflector().get<string[]>(
"abilities",
context.getHandler()
);
const ability = getReflector ? getReflector[0].toLowerCase() : null;
const apiHook = this.buildApiHook(ability);
return next.handle().pipe(
map(data => {
this.produceWebhook(webHooks, data, apiHook);
const emitter = webHooks.getEmitter();
let retries = Number(process.env.NUMBER_OF_RETRY_WEBHOOKS);
emitter.on("*.failure", (shortname, statusCode, body) => {
console.error(
`Error on trigger webHook ${shortname} with status code ${statusCode} and body ${body} - Remaining number of retries the webhooks : ${retries}`
);
const retryWebhooks = setTimeout(() => {
retries--;
this.produceWebhook(webHooks, data, apiHook);
}, Number(process.env.MS_RETRY_DELAY_WEBHOOKS));
if (retries === 0) {
clearTimeout(retryWebhooks);
}
});
return data;
})
);
}
private buildApiHook(ability: string) {
return ability !== null
? `hook.${ability.split(":")[1]}.${ability.split(":")[0]}`
: "hook";
}
}
Et il suffit ensuite d’utiliser ce morceau code sur les routes qui doivent déclencher la production d’un Webhooks
@UseInterceptors(new WebhooksProducerInterceptor())
Exemple de code pour consumer un Webhook (application externe)
var express = require("express");
var router = express.Router();
const checkValidWebhook = function (body, headers) {
return (
require("crypto")
.createHmac("sha256", "my_secret")
.update(JSON.stringify(body))
.digest("base64") === headers["myapi-signature"]
);
};
/* POST webhooks for consume */
router.post("/", async function (req, res, next) {
if (!checkValidWebhook(req.body, req.headers)) {
throw new Error("bad webhooks");
}
try {
// Your differents treatments based on req.headers['myapi-hook'] for consume the webhook
} catch (e) {
console.log(e);
}
res.send("webhook consume");
});
module.exports = router;
Epilogue
Comme vous l’avez vu, le principe même des Webhooks n’est pas si complexe que cela à comprendre mais également à mettre en place. Si vous mettez ce genre de système en place sur votre API, vous bénéficierez d’un système innovant et performant vous ouvrant des portes pour faire discuter votre architecture logicielle avec d’autres applications externe.