Middleware & Chain of Responsibility, ou la chaîne qui porte le transverseMiddleware & Chain of Responsibility, or the chain that carries the cross-cutting
Un handler qui n'existe que pour débiter une carte finit par authentifier, limiter, journaliser, valider, tracer — six préoccupations transverses entrelacées avec sa seule ligne de métier, recopiées dans chaque endpoint. La Chain of Responsibility — dont le middleware HTTP est une incarnation — sort chaque transverse dans un maillon autonome : la requête traverse une chaîne explicite, et le cœur n'est que le dernier maillon. On gagne réutilisation, test du cœur seul, ordre lisible. Le prix subtil : la chaîne devient si pratique qu'on y cache le flux de contrôle. Le middleware enveloppe la requête, il ne décide pas du métier.A handler that exists only to charge a card ends up authenticating, throttling, logging, validating, tracing — six cross-cutting concerns interwoven with its single business line, recopied in every endpoint. The Chain of Responsibility — of which HTTP middleware is one incarnation — lifts each concern into a standalone link: the request crosses an explicit chain, and the core is just the last link. You gain reuse, testing the core alone, a readable order. The subtle price: the chain becomes so convenient you hide control flow in it. Middleware wraps the request, it doesn't decide the domain.
AudienceAudience
Dev dont les handlers recopient auth, log et validation à l'identiqueDev whose handlers recopy auth, logging and validation identically
Un handler qui fait sept métiers. Lequel est vraiment le sien ?A handler doing seven jobs. Which one is actually its own?
handle_charge n'existe que pour une chose : débiter une carte. Mais avant d'y arriver, il authentifie, limite le débit, journalise, valide, trace, rattrape les erreurs. Six préoccupations transverses — communes à presque tous les handlers, étrangères au métier de celui-ci — entrelacées avec la seule ligne qui le justifie. Et chaque autre endpoint réécrit les six. Ce qui varie, ce n'est pas le métier : c'est l'ensemble des étapes transverses, qui se cumule, se réordonne, change indépendamment. Le réflexe : chaque préoccupation transverse devrait être un maillon composable d'une chaîne, ignorant les autres — et le cœur, au bout, ne ferait que son seul travail.handle_charge exists for one thing: charge a card. But before it gets there, it authenticates, throttles, logs, validates, traces, catches errors. Six cross-cutting concerns — common to nearly every handler, foreign to this one's domain — interwoven with the single line that justifies it. And every other endpoint rewrites all six. What varies isn't the domain: it's the set of cross-cutting steps, which accumulates, reorders, changes independently. The reflex: each cross-cutting concern should be a composable link in a chain, ignoring the others — and the core, at the end, would do only its one job.
Le code naïf — le cœur noyé sous le transverseThe naive code — the core drowned under cross-cutting
# un handler qui ne devrait charger une carte — et qui fait sept autres métiersdef handle_charge(req):
if not check_auth(req): return401# transverse : authentificationif rate_limited(req.ip): return429# transverse : quota
log.info("charge", req.id) # transverse : journalisationif not validate(req.body): return400# transverse : validation
with tracing.span("charge"): # transverse : observabilité
try:
charge(req.body.amount) # ← LE seul vrai métier, ligne 7
except Exception as e:
log.error(e); return500# transverse : gestion d'erreur# le cœur tient en une ligne ; les six autres reviennent dans CHAQUE handler.
Le cœur vs le transverseThe core vs the cross-cutting
charge(amount)charge(amount)
le cœur · une responsabilitéthe core · one responsibility
le transverse · volatil · se cumulecross-cutting · volatile · accumulates
Le cœur ne change pas si l'on ajoute une trace ou si l'on déplace l'authentification avant le quota ; le transverse, si. Tant que les deux vivent dans la même fonction, on ne peut tester le métier sans toute la cérémonie, ni changer la cérémonie sans rouvrir chaque handler.The core doesn't change if you add a trace or move auth before throttling; the cross-cutting does. As long as both live in the same function, you can't test the domain without the whole ceremony, nor change the ceremony without reopening every handler.
Le réflexe du volumeThe volume's reflex
« Cette couche connaît-elle des choses qu'elle ne devrait pas connaître ? » handle_charge connaît l'authentification, le quota, le format des logs, la trace — rien de tout cela n'est son métier. La Chain of Responsibility répond en sortant chaque transverse dans un maillon autonome : une requête traverse la chaîne, chaque maillon fait son unique travail puis passe la main (ou s'arrête), et le cœur n'est que le dernier maillon. C'est le réflexe du volume appliqué au chemin de la requête.'Does this layer know things it shouldn't know?' handle_charge knows authentication, throttling, log format, tracing — none of which is its domain. The Chain of Responsibility answers by lifting each cross-cutting concern into a standalone link: a request crosses the chain, each link does its single job then passes the baton (or stops), and the core is just the last link. It's the volume's reflex applied to the request path.
Ce n'est pas un truc de framework.This isn't a framework trick.
« Middleware » évoque app.use() d'Express, mais l'idée le précède de loin et n'a rien de spécifique au web : c'est la Chain of Responsibility du GoF. Une suite de maillons, chacun capable de traiter la requête ou de la passer au suivant. On la retrouve dans les passes d'un compilateur, les filtres d'image, les pipelines ETL, les intercepteurs gRPC. Ce numéro l'écrit en pseudo-code agnostique, pas autour d'un framework — le framework n'est qu'une des incarnations du motif, jamais le motif lui-même.'Middleware' evokes Express's app.use(), but the idea long predates it and is in no way web-specific: it's the GoF's Chain of Responsibility. A sequence of links, each able to handle the request or pass it on. You find it in compiler passes, image filters, ETL pipelines, gRPC interceptors. This issue writes it in agnostic pseudo-code, not around a framework — the framework is just one incarnation of the pattern, never the pattern itself.
№ 02La douleurThe pain4 min
Le transverse se duplique partout, et l'ordre des étapes n'est écrit nulle part.The cross-cutting duplicates everywhere, and the step order is written nowhere.
Garder le transverse dans chaque handler a trois coûts qui se renforcent. Il se duplique : auth, quota, log, validation sont recopiés dans chaque endpoint, et le jour où la règle change, on la cherche dans vingt fonctions. Le cœur devient intestable : pour vérifier « le débit refuse un montant négatif », il faut traîner toute la cérémonie d'authentification et de quota. Et l'ordre est implicite : l'auth doit passer avant le quota, le log doit envelopper l'erreur — cet enchaînement vit dans la tête des gens, jamais dans une structure qu'on peut lire ou réordonner sans risque.Keeping the cross-cutting in every handler has three costs that reinforce each other. It duplicates: auth, throttling, logging, validation are recopied in each endpoint, and the day the rule changes, you hunt for it in twenty functions. The core becomes untestable: to check 'the charge refuses a negative amount', you must drag the whole auth-and-throttle ceremony. And the order is implicit: auth must run before throttling, logging must wrap the error — that sequence lives in people's heads, never in a structure you can read or reorder safely.
№ 1 · Le transverse se dupliqueThe cross-cutting duplicates
Chaque handler recopie auth, quota, validation, journalisation. Vingt endpoints, vingt copies — et le jour où l'on durcit l'auth, ou qu'un log doit changer de format, il faut éditer les vingt sans en oublier une. La préoccupation transverse n'a pas de maison unique.Each handler recopies auth, throttling, validation, logging. Twenty endpoints, twenty copies — and the day you harden auth, or a log must change format, you edit all twenty without missing one. The cross-cutting concern has no single home.
№ 2 · Le cœur est intestable seulThe core is untestable alone
Tester « refuser un montant négatif » exige de simuler une requête authentifiée, sous le quota, validée, tracée. La règle la plus simple devient un test d'intégration parce qu'on ne peut pas la séparer de la cérémonie qui l'entoure.Testing 'refuse a negative amount' requires faking an authenticated, under-quota, validated, traced request. The simplest rule becomes an integration test because you can't separate it from the surrounding ceremony.
№ 3 · L'ordre est implicite et fragileThe order is implicit and fragile
L'auth avant le quota, le log autour de l'erreur, la validation avant le métier : cet ordre est partout sous-entendu, nulle part déclaré. Réordonner deux lignes dans un handler peut ouvrir une faille — et personne ne voit l'enchaînement complet d'un coup d'œil.Auth before throttling, logging around the error, validation before the domain: this order is everywhere implied, nowhere declared. Reordering two lines in a handler can open a hole — and no one sees the full sequence at a glance.
Le métier à la ligne 7, sous un mur de gardes recopiéesThe domain at line 7, under a wall of recopied guards
def handle_refund(req): # … et handle_ship, handle_cancel, handle_export …if not check_auth(req): return401# 💥 copie n°1if rate_limited(req.ip): return429# 💥 copie n°2
log.info(...); validate(...) # 💥 copies n°3, n°4
refund(req.body) # ← le métier, encore enseveli# durcir l'auth ? éditer les 20 handlers. en oublier un ? une faille silencieuse.
Le diagnostic.The diagnosis.
Le détecteur : « cette logique se répète-t-elle à l'identique dans plusieurs handlers, sans rien devoir au métier de chacun ? ». Si oui, c'est une préoccupation transverse mal rangée. Le réflexe : extraire chaque transverse en un maillon autonome, puis composer ces maillons en une chaîne explicite que la requête traverse — auth, puis quota, puis validation, puis le cœur. La règle se teste seule, en sautant la chaîne ; l'ordre devient une donnée qu'on lit et réordonne en un endroit ; et durcir l'auth, c'est éditer un maillon, pas vingt handlers.The detector: 'does this logic repeat identically across several handlers, owing nothing to each one's domain?'. If yes, it's a misfiled cross-cutting concern. The reflex: extract each cross-cutting concern into a standalone link, then compose those links into an explicit chain the request crosses — auth, then throttling, then validation, then the core. The rule tests alone, skipping the chain; the order becomes data you read and reorder in one place; and hardening auth means editing one link, not twenty handlers.
№ 03La chaîneThe chain5 min
On défait le bloc en maillons, et la requête les traverse.Break the block into links, and the request flows through them.
La Chain of Responsibility transforme un handler obèse en une suite de maillons composables. L'animation part du bloc où tout est entrelacé, en extrait chaque préoccupation transverse, les chaîne en un pipeline que la requête traverse de maillon en maillon, puis montre la liberté de composition : insérer une trace, réordonner, ou laisser un maillon court-circuiter la chaîne. À chaque marche, le cœur — débiter — reste le même maillon, à la fin, intouché.The Chain of Responsibility turns a fat handler into a sequence of composable links. The animation starts from the block where everything is interwoven, extracts each cross-cutting concern, chains them into a pipeline the request flows through link by link, then shows the freedom of composition: insert a trace, reorder, or let a link short-circuit the chain. At each step, the core — charging — stays the same link, at the end, untouched.
Animation — du bloc obèse au pipeline composableAnimation — from the fat block to the composable pipeline
Départ — tout dans un seul blocStart — everything in one block · 1/4
tout entrelacé · rien de réutilisableall interwoven · nothing reusable
handle() porte le métier ET les six préoccupations transverses, entrelacés. Une seule fonction sait tout : authentifier, limiter, journaliser, valider, tracer, et débiter. Rien n'est réutilisable, rien n'est testable seul.handle() carries the domain AND the six cross-cutting concerns, interwoven. One function knows everything: authenticate, throttle, log, validate, trace, and charge. Nothing is reusable, nothing is testable alone.
Un maillon : faire son travail, puis passer la main (ou s'arrêter) — 3 lignes à retrouverA link: do your job, then pass the baton (or stop) — 3 lines to recall
# un maillon : reçoit la requête et le « suivant », fait UNE chose, puis enchaîneif not check_auth(req): return401# court-circuit : on n'appelle pas next# la chaîne n'est qu'une composition de maillons — l'ordre est une donnéepipeline(req) # charge_handler, le cœur, ne fait QUE débiter — il ignore le reste.
3 ligne(s) à retrouver — écris-la de tête avant de cliquer3 line(s) to recall — write it in your head before clicking
Le motif transférable.The transferable pattern.
« Une suite de maillons à responsabilité unique, chacun libre de traiter ou de passer la main. » Selon le contexte, la même forme s'appelle Chain of Responsibility (GoF), pipeline, pile de Decorators, ou intercepteur. On la connaît en middleware HTTP, mais c'est exactement la structure des passes d'un compilateur, des filtres d'un traitement d'image, des étapes d'un ETL, des handlers d'un bus de messages (Vol 4 #2). Un seul principe : composer un comportement comme une chaîne, jamais comme une fonction-dieu.'A sequence of single-responsibility links, each free to handle or pass the baton.' Depending on context, the same shape is called Chain of Responsibility (GoF), pipeline, a stack of Decorators, or interceptor. We know it as HTTP middleware, but it's exactly the structure of a compiler's passes, an image processing filter chain, an ETL's stages, a message bus's handlers (Vol 4 #2). One principle: compose behavior as a chain, never as a god-function.
Modèle mentalMental model
L'imageThe image
Le contrôle des passagers à l'aéroport : billet, sécurité, douane, embarquement. Chaque poste fait UNE vérification puis laisse avancer — ou arrête tout net. Et le poste d'embarquement, au bout, ne revérifie pas le passeport : il fait confiance à la chaîne en amont et ne fait que son seul travail.Passenger control at the airport: ticket, security, customs, boarding. Each station makes ONE check then lets you through — or stops everything. And the boarding gate, at the end, doesn't recheck the passport: it trusts the chain upstream and does only its one job.
La règleThe rule
Chaque maillon fait une seule chose puis appelle le suivant — ou court-circuite. L'ordre est déclaré en un endroit lisible ; le cœur, dernier maillon, ne fait que son métier et ignore le reste.Each link does one thing then calls the next — or short-circuits. The order is declared in one readable place; the core, the last link, does only its job and ignores the rest.
Où l'analogie casseWhere the analogy breaks
À l'aéroport, chaque poste peut légitimement décider — refuser l'embarquement. Dans une chaîne logicielle, c'est l'inverse : un maillon qui décide du métier (une remise VIP) ou qui dépend en secret d'un voisin (il suppose qu'auth a posé req.user) reconstruit la fonction-dieu, distribuée et invisible. Le maillon filtre, journalise, mesure, protège ; il ne décide pas du métier, et ne parle aux autres que par ce qu'il passe explicitement — jamais par effet de bord.At the airport, each station can legitimately decide — refuse boarding. In a software chain it's the reverse: a link that decides the domain (a VIP discount) or secretly depends on a neighbor (it assumes auth set req.user) rebuilds the god-function, distributed and invisible. A link filters, logs, measures, protects; it doesn't decide the domain, and it talks to others only through what it passes explicitly — never through a side effect.
№ 04Le piègeThe trap4 min
Une chaîne n'est pas une cachette. Le maillon qui fait du métier en douce.A chain is not a hiding place. The link that quietly does business logic.
Le piège n'est pas la chaîne — c'est ce qu'on y glisse. Deux dérives se cumulent. D'abord les maillons se parlent par effet de bord : l'un mute un état partagé (req.user), un autre suppose qu'il a tourné avant — l'ordre devient une dépendance cachée, et réordonner casse en silence. Ensuite, le métier s'invite dans la chaîne : un « middleware » de remise prend une décision métier, hors du cœur, dans une couche que personne n'audite. La chaîne, censée porter le transverse, devient un second domaine clandestin où se cache le flux de contrôle. Bascule le toggle : cherche le maillon qui décide au lieu de filtrer.The trap isn't the chain — it's what you slip into it. Two drifts compound. First, links talk through side effects: one mutates shared state (req.user), another assumes it ran before — order becomes a hidden dependency, and reordering breaks silently. Then, the domain creeps into the chain: a 'discount middleware' makes a business decision, outside the core, in a layer no one audits. The chain, meant to carry cross-cutting concerns, becomes a clandestine second domain where control flow hides. Flip the toggle: hunt the link that decides instead of filters.
# des maillons VRAIMENT transverses, autonomes, dont l'ordre est explicitedef auth(req, next):
user = verify_token(req) # ne dépend de rien d'autre dans la chaîneif not user: return401return next(req.with_(user=user)) # passe l'info explicitement, pas par effet de bord
pipeline = compose([auth, rate_limit, validate, charge_handler])
# chaque maillon : une préoccupation transverse, indépendante, réutilisable partout.# le métier (montant, devise, idempotence) reste DANS charge_handler — jamais ici.
Pourquoi c'est justeWhy it's right
Chaque maillon est une vraie préoccupation transverse — auth, quota, validation — autonome et réutilisable sur n'importe quel handler. Quand auth a besoin de transmettre l'utilisateur, il le passe explicitement (req.with_(user=…)), pas par un effet de bord que le maillon suivant devine. L'ordre est déclaré en un endroit, lisible et réordonnable. Et le métier — montant, devise, idempotence — reste captif du cœur, charge_handler, jamais dispersé dans la chaîne. La chaîne porte ce qui entoure la requête ; le cœur porte ce qu'elle veut dire.Each link is a genuine cross-cutting concern — auth, throttling, validation — standalone and reusable on any handler. When auth needs to pass the user along, it passes it explicitly (req.with_(user=…)), not through a side effect the next link guesses. The order is declared in one place, readable and reorderable. And the domain — amount, currency, idempotence — stays captive in the core, charge_handler, never scattered in the chain. The chain carries what surrounds the request; the core carries what it means.
La règle qui tranche.The rule that settles it.
Un maillon mérite sa place seulement s'il est une vraie préoccupation transverse — il s'applique à de nombreux handlers, indépendamment du métier — ET qu'il ne dépend pas en secret de la mutation d'un autre. Le test : « si je retire ou réordonne ce maillon, l'échec est-il bruyant ou silencieux ? ». S'il dépend en douce d'un effet de bord d'un voisin, tu n'as pas un pipeline composable, tu as une fonction-dieu distribuée. Et la frontière nette : un maillon filtre, journalise, mesure, protège ; il ne décide pas du métier. La décision métier reste dans le cœur.A link earns its place only if it's a true cross-cutting concern — it applies across many handlers, independent of the domain — AND it doesn't secretly depend on another's mutation. The test: 'if I remove or reorder this link, is the failure loud or silent?'. If it quietly depends on a neighbor's side effect, you don't have a composable pipeline, you have a distributed god-function. And the sharp line: a link filters, logs, measures, protects; it doesn't decide the domain. The business decision stays in the core.
Le piège jumeau : la chaîne pour la chaîne.The twin trap: chaining for its own sake.
L'autre excès, inverse : huit maillons pour un endpoint trivial, parce que « c'est l'architecture ». Chaque requête traverse alors une procession d'indirections dont la moitié ne s'applique pas — de la cérémonie (YAGNI, Vol 0 #3) qui ajoute des sauts sans payer son loyer (Vol 0 #2). Et attention à ne pas confondre la chaîne avec une séquence métier : si les étapes ont un ordre imposé par le domaine et doivent se défaire à l'échec, ce n'est pas du middleware, c'est de l'orchestration (Vol 4 #3). Le middleware enveloppe la requête ; il ne porte pas la transaction.The opposite excess: eight links for a trivial endpoint, because 'it's the architecture'. Every request then crosses a procession of indirections half of which don't apply — ceremony (YAGNI, Vol 0 #3) that adds hops without paying its rent (Vol 0 #2). And don't mistake the chain for a business sequence: if the steps have a domain-imposed order and must undo on failure, that's not middleware, it's orchestration (Vol 4 #3). Middleware wraps the request; it doesn't carry the transaction.
№ 05Bilan & éditoWrap-up & editorial2 min
Un handler qui sait tout, ou une chaîne où chacun sait une chose.A handler that knows everything, or a chain where each knows one thing.
Handler obèse vs pipeline de maillons, côte à côteFat handler vs link pipeline, side by side
handler obèsefat handler
chaîne de maillonschain of links
Le transverseCross-cutting
recopié dans chaque handlerrecopied in each handler
un maillon, une maisonone link, one home
Tester le cœurTest the core
traîner toute la cérémoniedrag the whole ceremony
le maillon seul, hors chaînethe link alone, off-chain
L'ordre des étapesThe step order
implicite, dans les têtesimplicit, in heads
déclaré en un endroitdeclared in one place
Ajouter une traceAdd a trace
éditer N handlersedit N handlers
insérer un mailloninsert a link
Le dangerThe danger
duplicationduplication
cacher le flux / le métierhiding flow / domain
Quand ne pas chaînerWhen not to chain
La chaîne a un loyer : une indirection de plus à chaque maillon, et une structure de composition à tenir. On reste avec un appel direct — et c'est correct — quand :The chain has rent: one more indirection per link, and a composition structure to maintain. Stay with a direct call — and that's fine — when:
il n'y a qu'une seule préoccupation transverse, ou aucune : un endpoint isolé qui ne fait que lire une donnée publique n'a pas besoin d'un pipeline. Une garde inline est plus courte et plus franche — chaîner ici, c'est de la cérémonie (YAGNI, Vol 0 #3).there's only one cross-cutting concern, or none: an isolated endpoint that just reads public data needs no pipeline. An inline guard is shorter and franker — chaining here is ceremony (YAGNI, Vol 0 #3).
les « étapes » sont en réalité une séquence métier : si l'ordre est imposé par le domaine et qu'un échec doit défaire les précédents, ce n'est pas du transverse, c'est une transaction — donc de l'orchestration (Vol 4 #3), pas un middleware. La chaîne enveloppe la requête ; elle ne porte pas le processus métier.the 'steps' are actually a business sequence: if the order is domain-imposed and a failure must undo the prior ones, it's not cross-cutting, it's a transaction — so orchestration (Vol 4 #3), not middleware. The chain wraps the request; it doesn't carry the business process.
la chaîne servirait à cacher du flux de contrôle : si tu chaînes pour rendre un branchement métier « invisible », tu construis une fonction-dieu distribuée. Mieux vaut un cœur honnête qu'un pipeline qui dissimule où les décisions se prennent.the chain would serve to hide control flow: if you chain to make a business branch 'invisible', you're building a distributed god-function. Better an honest core than a pipeline that conceals where decisions are made.
Le réflexe n'est donc pas « mets tout en middleware, c'est modulaire » : c'est « cette préoccupation est-elle vraiment transverse et indépendante du métier ? Alors un maillon. Sinon, elle appartient au cœur, ou à un orchestrateur — pas à la chaîne. »So the reflex isn't 'put everything in middleware, it's modular': it's 'is this concern truly cross-cutting and independent of the domain? Then a link. Otherwise it belongs in the core, or an orchestrator — not the chain.'
Mot de l'éditeurFrom the editor
Ce numéro applique le réflexe du volume — « cette couche connaît-elle des choses qu'elle ne devrait pas connaître ? » — au chemin de la requête. Un handler qui authentifie, journalise et limite le débit connaît trop ; la chaîne lui rend sa seule responsabilité en sortant le transverse dans des maillons. Mais c'est aussi le pattern le plus marqué par les frameworks, et son danger est subtil : la chaîne devient si pratique qu'on y range le flux de contrôle et les décisions métier, jusqu'à reconstruire — distribuée et invisible — la fonction-dieu qu'on fuyait. La discipline tient en une phrase : le middleware enveloppe la requête, il ne décide pas du métier.This issue applies the volume's reflex — 'does this layer know things it shouldn't know?' — to the request path. A handler that authenticates, logs and throttles knows too much; the chain gives it back its one responsibility by lifting the cross-cutting into links. But it's also the pattern most marked by frameworks, and its danger is subtle: the chain becomes so convenient you file control flow and business decisions in it, until you rebuild — distributed and invisible — the very god-function you fled. The discipline fits in one sentence: middleware wraps the request, it doesn't decide the domain.
Avec lui se referme le volume des responsabilités : le #1 a posé les couches et montré où elles fuient, le #2 a opposé l'axe horizontal (par couche) à l'axe vertical (par feature), le #3 a sorti le transverse du cœur. Trois manières de répondre à la même question — qui a le droit de connaître quoi. La suite descend d'un cran, du système vers l'objet : le Vol 3 garde les quelques patterns du GoF dont le réflexe survit, à commencer par celui qui remplace un if/else par un comportement injectable — Strategy. On y verra qu'un maillon de chaîne, au fond, n'est qu'une stratégie de plus, passée en paramètre.With it the responsibilities volume closes: #1 laid down the layers and showed where they leak, #2 opposed the horizontal axis (by layer) to the vertical one (by feature), #3 lifted the cross-cutting out of the core. Three ways to answer the same question — who is allowed to know what. What follows steps down a notch, from system to object: Vol 3 keeps the few GoF patterns whose reflex survives, starting with the one that replaces an if/else with an injectable behavior — Strategy. We'll see that a chain link is, at bottom, just one more strategy, passed as a parameter.
Les voisins que ce numéro débloqueThe neighbors this issue unlocks
Pipeline — la même chaîne vue comme un flux de données : chaque étape transforme l'entrée et la passe à la suivante. Là où le middleware filtre et protège une requête, le pipeline raffine une valeur — passes de compilateur, traitement d'image, ETL. Une seule forme, deux usages.the same chain seen as a data flow: each stage transforms the input and passes it on. Where middleware filters and protects a request, the pipeline refines a value — compiler passes, image processing, ETL. One shape, two uses.
Decorator — le maillon vu comme un emballage : envelopper un objet pour lui ajouter un comportement (cache, log, retry) sans toucher à son code. Une pile de Decorators est une chaîne de responsabilité qui s'applique à un objet plutôt qu'à une requête — le cousin objet du middleware.the link seen as a wrapper: wrap an object to add behavior (cache, log, retry) without touching its code. A stack of Decorators is a chain of responsibility applied to an object rather than a request — middleware's object-level cousin.
Interceptor — le maillon des appels distants : s'insérer autour d'un appel RPC/gRPC pour y ajouter authentification, tracing, retry, des deux côtés du fil. Le même motif, déplacé du serveur HTTP vers la frontière réseau — utile dès qu'on entre dans le distribué (Vol 6, Vol 7).the link for remote calls: insert around an RPC/gRPC call to add authentication, tracing, retry, on both sides of the wire. The same pattern, moved from the HTTP server to the network boundary — useful once you enter the distributed world (Vol 6, Vol 7).
Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it
Tu as vu le bloc obèse se défaire en maillons au chapitre 3 et retrouvé les trois lignes qui font la chaîne. Reste le geste qui ancre : l'écrire. Copie ce fichier de tests dans ton langage habituel — la spec tient dans les noms des tests — et fais-le passer au vert sans revenir au numéro. Le dernier test est le sournois : réordonner deux vrais transverses ne doit RIEN changer au résultat. S'il change, c'est que tes maillons se parlent en secret — le piège du chapitre 4.You watched the fat block break into links in chapter 3 and recalled the three lines that make the chain. What's left is the gesture that anchors it: writing it. Copy this test file into your usual language — the spec fits in the test names — and turn it green without coming back to the issue. The last test is the sneaky one: reordering two genuine cross-cutting links must change NOTHING in the result. If it does, your links talk to each other in secret — chapter 4's trap.
test_middleware_chain.py
# test_middleware_chain.py — la spec EST le harnais : fais-le passer au vert.# une chaîne de maillons composables : chacun fait UNE chose, puis passe la main. sans framework.def test_la_requete_traverse_dans_l_ordre():
trace = []
tap = lambda name: lambda req, nxt: (trace.append(name), nxt(req))[1]
compose([tap("a"), tap("b"), tap("c")])(Req())
assert trace == ["a", "b", "c"] # l'ordre déclaré = l'ordre d'exécutiondef test_le_coeur_est_le_dernier_maillon():
charged = []
core = lambda req, nxt: charged.append(req.amount)
compose([auth_ok, core])(Req(amount=42))
assert charged == [42] # le cœur ne fait QUE son métierdef test_un_maillon_court_circuite():
reached = []
core = lambda req, nxt: reached.append(True)
resp = compose([deny, core])(Req()) # deny renvoie 401 sans appeler nxt
assert resp == 401 and reached == [] # la suite n'est jamais atteintedef test_un_maillon_passe_l_info_explicitement():
seen = {}
def auth(req, nxt): return nxt(req.with_(user="alice"))
def peek(req, nxt): seen["u"] = req.user; return nxt(req)
compose([auth, peek])(Req())
assert seen["u"] == "alice"# transmis PAR la requête, pas par effet de borddef test_l_ordre_est_une_decision_declaree():
counted = []
quota = lambda req, nxt: (counted.append(req), nxt(req))[1]
compose([deny, quota, core])(Req()) # auth AVANT le quota…
assert counted == [] # …un anonyme refusé ne consomme rien
compose([quota, deny, core])(Req()) # inversé : il consomme du quota
assert len(counted) == 1# l'ordre n'est pas neutre : il se déclare, en un endroit
Chez toi, cette semaine.In your codebase, this week.
Ouvre ton dossier middleware/ (ou tes app.use, tes intercepteurs) et cherche le maillon qui décide au lieu de filtrer : grep -rn "is_\|role ==\|amount\|price\|discount" middleware/. Un maillon qui lit un montant ou un rôle pour trancher une remise fait du métier hors du cœur. Puis le test qui tranche, gratuit : dans un test, réordonne deux maillons transverses ; si un résultat métier change, l'ordre était une dépendance cachée — tu as une fonction-dieu distribuée.Open your middleware/ folder (or your app.use calls, your interceptors) and hunt the link that decides instead of filters: grep -rn "is_\|role ==\|amount\|price\|discount" middleware/. A link that reads an amount or a role to settle a discount is doing business logic outside the core. Then the free test that settles it: in a test, reorder two cross-cutting links; if a business result changes, order was a hidden dependency — you have a distributed god-function.