Supposons qu’Alice appartienne à alpha. Elle demande /workspaces/alpha/documents/doc-b/, mais reçoit un document de beta. Le serveur a reconnu Alice et vérifié son appartenance. Les deux contrôles ont réussi. Où la requête a-t-elle déraillé ?
Cet exemple utilise des documents et identités fictifs. Il nécessite Node.js 22 ou ultérieur ; la méthode s’applique aussi à d’autres langages et frameworks. La question utile est précise : quel espace a été vérifié et lequel a limité la recherche du document ?
Partir d’une requête et de la règle à respecter
Gardez ensemble méthode, chemin, identité et résultat attendu :
GET /workspaces/alpha/documents/doc-b/
Demo identity: alice, a member of alpha only
Stored document: doc-b belongs to beta
Expected: 404, with no document in the response
Broken result: 200, containing Beta release notes
La politique donne aux membres accès à tous les documents de leur espace, sans partage particulier ni exception administrateur. Pour un membre d’alpha, un document hors d’alpha produit le même statut et corps qu’un document absent. Une requête visant un espace non rejoint reçoit 403 avant l’appel au stockage.
Il faut suivre deux relations : l’utilisateur authentifié appartient à l’espace de l’URL, et le document renvoyé appartient à ce même espace. Réussir le premier contrôle laisse le second à la recherche de données.
Dans un vrai rapport, notez aussi la version de l’application ou du source. La route locale doit correspondre au code ayant traité la requête. Utilisez une fixture synthétique pour la reproduction.
Trouver l’enregistrement de route avant le gestionnaire
Téléchargez request-flow.mjs et request-flow.test.mjs dans le même dossier. Cherchez les mots distinctifs du chemin, comme workspaces et documents, dans l’éditeur. Avec ripgrep, ces commandes resserrent les prochaines étapes :
rg -n 'workspaces|documents' request-flow.mjs
rg -n 'middleware|runMiddleware|requireWorkspaceMember|findById|findInWorkspace' request-flow.mjs
Ouvrez createApp. Son objet route réunit méthode, motif de chemin, middleware et gestionnaire choisi :
const route = {
method: 'GET',
pattern: /^\/workspaces\/([a-z0-9-]+)\/documents\/([a-z0-9-]+)\/$/,
middleware: [authenticate, requireWorkspaceMember],
handler: lookup === 'unscoped' ? getDocumentUnsafe : getDocument,
};
Lisez aussi la suite. handle compare new URL(request.url).pathname, vérifie la méthode et assigne les deux segments capturés à workspaceId et documentId. Aucun ne vient des paramètres de requête. L’exemple accepte lettres minuscules, chiffres et tirets, et exige la barre oblique finale ; ce n’est pas un routeur généraliste.
Dans un framework, le chemin peut être partagé entre un montage parent comme /workspaces/:workspaceId et une route enfant comme /documents/:documentId. Lisez le montage et vérifiez le passage des paramètres parents. Un résultat dans le gestionnaire ne révèle pas les middlewares entourant son appelant.
Lire les middlewares dans leur ordre réel
runMiddleware enveloppe le gestionnaire avec reduceRight. Avec [authenticate, requireWorkspaceMember], viennent l’authentification, l’appartenance puis le gestionnaire, si chaque étape appelle next(). Les valeurs de return next() remontent à handle, dont le catch transforme un HttpError lancé en réponse.
Request → path and method match → authenticate
→ requireWorkspaceMember → document lookup → JSON Response
↘ thrown error → handle's catch → error Response
authenticate cherche l’en-tête Authorization dans une map fixe et place le résultat dans ctx.user. demo-alice et demo-bob sont des entrées publiques de fixture représentant une session vérifiée. Cela suffit pour suivre l’identité ici ; la vérification réelle de session ou jeton doit être lue séparément.
Ouvrez maintenant le middleware d’appartenance :
async function requireWorkspaceMember(ctx, next) {
const { workspaceId } = ctx.params;
const allowed = ctx.user.workspaceIds.includes(workspaceId);
ctx.trace.push(`membership:${workspaceId}:${allowed ? 'allowed' : 'denied'}`);
if (!allowed) throw new HttpError(403, 'Forbidden');
return next();
}
Pour Alice et un chemin nommant alpha, allowed vaut true. Rien n’examine documentId ni ne charge un document. Notez précisément : « Alice peut entrer dans la route alpha. » Écrire seulement « autorisée » invite à surestimer le contrôle.
Marquez aussi les arrêts. Une identité absente ou inconnue donne 401 avant l’appartenance ; un non-membre reçoit 403 avant le gestionnaire. Si le middleware appelle next() avant sa décision, le gestionnaire peut déjà avoir tourné. La position compte autant que le nom.
Distinguer utilisateur, espace demandé et document stocké
Une note utile pour la requête fautive tient en quatre lignes :
| Valeur | Origine | Valeur de la requête | Où la suivre |
|---|---|---|---|
| Utilisateur et appartenances | authenticate lit la map d’identités |
Alice, ['alpha'] |
requireWorkspaceMember |
| Espace demandé | Première capture de route | alpha |
Appartenance, puis arguments de recherche |
| Document demandé | Seconde capture de route | doc-b |
Recherche de document |
| Espace réel du document | Ligne stockée | beta |
Prédicat de recherche et réponse |
Ajoutez fichiers et lignes en inspectant chaque producteur et consommateur. Dans une grande application, ce seront peut-être des types et paquets différents. La distinction reste utile : l’URL exprime la demande, l’identité les appartenances, la ligne stockée la propriété.
Les deux identifiants arrivent ensemble. Voyez maintenant s’ils quittent le gestionnaire ensemble.
Ouvrir la recherche, même si le gestionnaire semble ordinaire
Le gestionnaire volontairement non sûr est :
async function getDocumentUnsafe(ctx) {
ctx.trace.push(`lookup:unscoped:${ctx.params.documentId}`);
const document = await ctx.store.findById(ctx.params.documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
Seul documentId atteint le stockage. Suivez findById dans createStore : le prédicat est document.id === documentId. Aucun contrôle d’espace ni vérification ultérieure de propriété ne précède la sérialisation. Le middleware a accepté alpha ; cette recherche peut renvoyer n’importe quel espace.
Ni course ni session falsifiée ne sont nécessaires. Alice garde son identité et le chemin autorisé alpha, puis fournit l’identifiant doc-b. L’application vérifie un périmètre de ressource et en lit un autre.
Dans un vrai projet, un nom de fonction serait une preuve plus faible. Une méthode peut ajouter un filtre de locataire, hériter d’une session de base restreinte ou dépendre de politiques par ligne. Lisez implémentation et configuration avant de conclure qu’un argument absent implique une requête non limitée. Ici, le prédicat en mémoire est toute la recherche, donc directement vérifiable.
Exécuter le contre-exemple sur toute la chaîne
Depuis le dossier contenant les deux fichiers, lancez :
node request-flow.mjs
node --test request-flow.test.mjs
Aucune installation, compte ou service. makeRequest construit un Request Web, transmis à handle, qui renvoie une Response Web. L’URL example.invalid est uniquement analysée en mémoire. L’exemple n’appelle jamais fetch et n’ouvre aucun socket d’écoute.
La démo affiche ces réponses, avec les étapes visitées et les appels au stockage sous chacune :
normal: 200 {"id":"doc-a","workspaceId":"alpha","title":"Alpha release notes"}
reproduce: 200 {"id":"doc-b","workspaceId":"beta","title":"Beta release notes"}
fixed: 404 {"error":"Not found"}
Pour la requête fautive, les étapes visitées sont :
request:GET -> route:alpha:doc-b -> authenticate:alice -> membership:alpha:allowed -> lookup:unscoped:doc-b -> response:200
Le tableau de trace est une instrumentation de la fixture, conservée hors réponse. Le test vérifie aussi le document et l’appel enregistré à findById('doc-b'). Ces assertions relient la séquence de branches aux données réellement renvoyées.
Le test reproduce: passe quand la version non sûre renvoie le document de beta. Son vert confirme le défaut reproductible. Un test séparé exige 404 de la version corrigée pour la même requête.
Transmettre l’espace vérifié à la requête de données
Le gestionnaire corrigé conserve les deux valeurs de route :
async function getDocument(ctx) {
const { workspaceId, documentId } = ctx.params;
ctx.trace.push(`lookup:scoped:${workspaceId}:${documentId}`);
const document = await ctx.store.findInWorkspace(workspaceId, documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
Dans findInWorkspace, le prédicat exige document.workspaceId === workspaceId et document.id === documentId. Le middleware a déjà vérifié ce même workspaceId. La fixture ne modifie pas ctx.params entre ces étapes ; documents et appartenances restent fixes durant chaque exécution.
Pour /workspaces/alpha/documents/doc-b/, il n’y a plus de résultat. Un identifiant absent ou d’un autre espace produit 404 avec {"error":"Not found"}. Statut et corps identiques évitent de révéler explicitement le cas ; les tests ne disent rien du temps ou d’autres canaux auxiliaires.
Vérifier document.workspaceId après une lecture globale pourrait aussi bloquer cette fuite précise. Une recherche ciblée rend la restriction explicite au stockage et ne renvoie aucune ligne étrangère au gestionnaire. Ici, Array.find examine toujours les lignes pour évaluer le prédicat. Les appels enregistrés montrent les arguments, pas les E/S de base ni l’isolation physique.
Pour SQL ou un ORM, inspectez la condition émise et la liaison des paramètres, puis testez avec la vraie couche de données. Révocation d’appartenance, déplacements, partages et autres routes exigent leurs propres règles et tests.
Tester le succès et les arrêts obligatoires
La suite contient dix tests : une reproduction volontaire et neuf contrôles de l’app corrigée. Plusieurs couvrent plus d’une entrée :
| Requête ou condition | Réponse attendue | Comportement du stockage |
|---|---|---|
Alice lit alpha/doc-a ; Bob lit beta/doc-b |
200, bon document |
Les deux ID atteignent findInWorkspace |
Alice lit alpha/doc-b |
404, aucun document |
Recherche limitée à alpha |
Alice lit alpha/missing |
Même statut et corps que ci-dessus | Aucun document correspondant |
| Identité absente ou inconnue | 401 |
Aucun appel au stockage ; appartenance non atteinte |
Alice lit beta/doc-b |
403 |
Aucun appel au stockage |
Alice lit alpha/doc-b?workspaceId=beta |
404 |
Le paramètre ne remplace pas le périmètre de route |
| Chemin inconnu ; mauvaise méthode sur chemin connu | 404 ; 405 avec Allow: GET |
Aucun appel au stockage |
| Erreur inattendue du stockage | 500, corps générique |
L’erreur atteint l’enveloppe de réponse |
Les chemins abrégés correspondent à /workspaces/{workspaceId}/documents/{documentId}/, avec la requête après la barre finale. Les deux succès empêchent une « correction » rejetant tout. Les assertions d’absence d’appel détectent un rejet seulement après accès au stockage. Ensemble, elles vérifient plus que les statuts finaux.
Ces tests couvrent le traitement en processus, l’ordre des middlewares, les arguments et les réponses JSON de cette route. Ils ne testent ni transport HTTP, proxy inverse, identité de production ni base de données. Dans une application, partez de l’adaptateur HTTP réel et vérifiez le choix du gestionnaire et des middlewares suivis.
Laisser une note vérifiable
Présentez le résultat autour de la requête et des points précis d’utilisation de ses valeurs :
Trigger: Alice + GET /workspaces/alpha/documents/doc-b/
Route: createApp captures alpha and doc-b.
Membership: requireWorkspaceMember allows Alice for alpha.
Old read: getDocumentUnsafe passes only doc-b to findById.
Observed: 200 returns a row whose workspaceId is beta.
Fix: getDocument passes alpha and doc-b to findInWorkspace.
Regression: same request returns 404; both valid member reads still return 200.
Scope: in-memory request pipeline; production adapter and storage are untested.
Joignez les emplacements source et la commande de test. Si quelqu’un demande « Cette route n’a-t-elle pas déjà une autorisation ? », la note montre exactement ce que couvre le contrôle et où l’espace cesse d’être utilisé.