HTTP, web, JS/TS & sécurité fiche thématique 11 · 20 questions · v1 du 11/09/2026 · couvre les cartes Anki « swe » web::http, auth, authz, middleware, api-design, cors, hashing, validation, javascript::*, typescript::*, react::*, npm, db::pagination

Fil rouge : une API Node/TypeScript type Medusa avec middleware d'authentification, un front React qui charge des données avec react-query, et un stockage de mots de passe et de tokens.

01

Sémantique HTTP

QQue t'apprend la classe du code (4xx vs 5xx) sur l'endroit de la faute ?

4xx = faute côté client : requête mal formée, non authentifiée, ressource inexistante — corriger la requête. 5xx = faute côté serveur ; la réponse est volontairement muette (« unknown error ») pour ne pas exposer l'interne — aller lire les logs.

Au tableau

« Le code dit qui doit agir, donc un 4xx renvoie le client à sa requête et un 5xx renvoie le développeur à ses logs. »

Q401 vs 403 ?

401 = je ne sais pas qui tu es (authentification) : réessayer avec des identifiants. 403 = je sais qui tu es et tu n'as pas le droit (autorisation) : réessayer ne sert à rien. Le nom « 401 Unauthorized » est historiquement mal choisi.

Au tableau

« L'authentification établit l'identité et l'autorisation vérifie les droits, donc 401 manque l'identité et 403 manque le droit. »

QUn client envoie DELETE, la réponse se perd, il réessaie. Que répondre ?

Un succès, pas un 404. DELETE signifie « fais que cette ressource n'existe plus » : l'état visé est atteint. C'est l'idempotence : GET, PUT, DELETE le sont ; POST ne l'est pas — d'où les clés d'idempotence pour les paiements.

Au tableau

« Une requête idempotente décrit un état cible, donc la rejouer ne change rien, donc l'état atteint est un succès même si la ressource a déjà disparu. »

QStripe et Medusa mettent à jour avec POST /resource/:id, jamais PATCH. Quel raisonnement ?

REST n'est pas une norme mais un style : aucune autorité ne valide la conformité, chaque plateforme arbitre. Ce qu'on gagne : un seul verbe, moins de surface, pas de débat PUT/PATCH. Ce qu'on perd : POST signifie création ou mise à jour selon que le chemin porte un id, et POST n'est pas idempotent par convention.

Au tableau

« Le style REST ne contraint pas les verbes, donc une plateforme peut choisir la simplicité, donc POST partout, au prix d'une sémantique qui se lit dans le chemin. »

QLe middleware s'exécute-t-il avant ou après le routing ? Conséquence observable ?

Avant. Il peut interrompre la requête avant que le serveur sache si la route existe : une URL inexistante sans l'en-tête exigé renvoie l'erreur du middleware (400/401), pas 404.

Au tableau

« Le middleware est traversé par toute requête avant la résolution de route, donc son verdict précède le 404, donc une route absente peut répondre 401. »

02

Runtime JS / TS — ce qui n'existe pas à l'exécution

QPromise { <pending> } ou [object Promise] dans un log : que signale-t-il ?

Un await oublié. Une fonction async retourne toujours une promesse ; sans await on manipule la promesse, pas sa valeur. Le symptôme apparaît à la première concaténation ou sérialisation.

Au tableau

« async enveloppe toute valeur de retour dans une promesse, donc sans await on obtient l'enveloppe, donc le log affiche un objet Promise. »

Q?? vs || : quand basculer sur l'opérande de droite ?

?? bascule seulement si la gauche vaut null ou undefined ; || bascule sur toute valeur falsy (0, '', false). Pour un remplaçant sur un champ nullable, ?? : une chaîne vide ou un 0 restent des valeurs.

Au tableau

« 0 et la chaîne vide sont des valeurs légitimes, donc un || les écraserait, donc on utilise ?? qui ne teste que l'absence. »

Qconst xs = [] sans annotation en TypeScript : quel type, quelle conséquence ?

never[] — valide tant qu'on ne lit rien dedans : never est le type sans aucune valeur, on ne peut lire aucune propriété. Annoter const xs: string[] = [].

Au tableau

« Un tableau vide n'a aucun élément pour inférer un type, donc TS choisit never, donc toute lecture est une erreur de type. »

QUn endpoint typé reçoit {} alors que le type déclare trois champs obligatoires. Pourquoi rien ne l'arrête ?

Les types TypeScript sont entièrement effacés à la compilation : il n'en reste rien à l'exécution. Un type décrit un contrat entre développeurs ; un schéma runtime (zod) le fait respecter face à un client. Les deux doivent rester alignés (inférer le type depuis le schéma).

Au tableau

« Le type n'existe que pour le compilateur, donc un client externe ne le voit jamais, donc il faut valider les données à l'entrée avec un schéma exécuté. »

QUn import npm qui fonctionne aujourd'hui peut cesser de résoudre demain sans toucher au code. Pourquoi ?

Le paquet n'est pas déclaré dans package.json : il n'est présent que parce que npm l'a installé comme dépendance transitive d'un autre. Une mise à jour de cet autre peut le retirer. Déclarer tout import direct transforme « ça marche par hasard » en contrat.

Au tableau

« Une dépendance transitive appartient à quelqu'un d'autre, donc elle peut disparaître à sa prochaine version, donc tout ce qu'on importe doit être déclaré chez soi. »

03

React et fetch

QComment un composant React affiche-t-il une donnée asynchrone alors que sa fonction est synchrone ?

Il n'attend pas. Au premier appel il s'exécute entièrement avec la donnée absente, retourne un état de chargement ; quand la donnée arrive, un changement d'état déclenche un nouveau rendu. C'est pourquoi un composant ne peut pas être async : React a besoin d'un retour immédiat.

Au tableau

« Le rendu doit produire un arbre tout de suite, donc on rend l'absence, donc l'arrivée de la donnée provoque un second rendu. »

QDeux useQuery de même queryKey dans le même rendu : combien de requêtes, et que vaut isPending ?

Une seule requête, et isPending vaut quand même true pour les deux. Le second trouve l'entrée dans le cache, mais l'entrée est encore vide : trouver l'entrée n'est pas trouver la donnée. C'est de la déduplication, pas un hit de cache.

Au tableau

« Le cache est indexé par clé dès la première demande, donc la seconde s'y attache, donc une seule requête part, mais la donnée n'est pas encore là, donc les deux attendent. »

QDans quels cas la promesse de fetch() est-elle rejetée ?

Uniquement sur un échec de transport : DNS, connexion coupée, blocage CORS. Toute réponse HTTP, 404 ou 500 comprise, est une résolution. Il faut tester res.ok et lever soi-même, sinon res.json() parse un corps d'erreur.

Au tableau

« fetch réussit dès qu'une réponse arrive, donc un 500 est un succès pour lui, donc l'erreur métier se lit dans res.ok, pas dans le catch. »

QValeur par défaut de credentials dans fetch, et que décide-t-elle ?

same-origin : les cookies partent seulement vers la même origine (schéma + hôte + port). Vers une autre origine il faut credentials: 'include' et un serveur qui l'autorise (CORS). Le cookie appartient au navigateur ; la requête décide seulement s'il l'accompagne.

Au tableau

« Une origine est le triplet schéma-hôte-port, donc un port différent est une autre origine, donc les cookies ne partent pas sans include. »

QUne route renvoie à l'utilisateur ses enregistrements : lecture puis filtre en mémoire. Quel bug silencieux ?

Si l'ORM applique une taille de page par défaut, la limite s'applique à la population entière, avant le filtre : l'utilisateur ne voit que ses lignes tombées dans la première page. C'est un 200 avec un JSON bien formé, aucune erreur, aucun log — et ça dépend de la volumétrie. Filtrer dans la requête (WHERE owner_id = ?).

Au tableau

« La pagination coupe avant que le filtre applicatif ne voie les lignes, donc des enregistrements légitimes disparaissent sans erreur, donc le filtre doit être dans la requête. »

04

Secrets et autorisation

Qbcrypt et argon2 : en quoi diffèrent-ils de SHA-256 ?

Fonctions de hachage de mots de passe : délibérément lentes, coût réglable (facteur de travail), sel intégré. SHA-256 est conçu rapide : des milliards d'essais par seconde sur GPU. bcrypt.hash(pwd) renvoie une chaîne contenant coût, sel et digest.

Au tableau

« Un mot de passe est devinable, donc la défense est de rendre chaque essai coûteux, donc on choisit une fonction lente à coût réglable. »

QQu'apporte exactement un sel, et qu'est-ce qu'il n'apporte pas ?

Il désolidarise les lignes : deux utilisateurs de même mot de passe ont des digests différents, donc pas de table précalculée ni d'attaque « une pour tous ». Sans sel, 10⁹ hachages couvrent 10⁷ utilisateurs d'un coup. Il ne rend pas un mot de passe faible fort : un attaquant qui vise une ligne n'est pas ralenti.

Au tableau

« Le sel rend chaque digest unique, donc l'attaquant doit recommencer par ligne, donc il perd le parallélisme, mais la force par ligne vient du coût, pas du sel. »

QToken porteur opaque de 32 octets aléatoires : pourquoi le hacher en SHA-256 et non bcrypt ?

bcrypt défend des secrets devinables, à faible entropie ; 32 octets aléatoires ont 256 bits d'entropie, indevinables. Décisif : le token est la clé de recherche — on doit retrouver la ligne par le hash, donc le hash doit être déterministe sans sel ; bcrypt, salé, oblige à tester chaque ligne. Avec un mot de passe, l'e-mail désigne la ligne, puis on compare.

Au tableau

« Un secret à haute entropie n'a pas besoin d'être ralenti, donc SHA-256 suffit, et le hash doit servir d'index, donc il doit être déterministe. »

QUn secret cryptographique en Node : d'où le tirer, et pourquoi pas Math.random ?

crypto.randomBytes : un secret ne vaut jamais plus que sa source d'aléa. Math.random couvre au mieux 2⁵³ états, souvent moins, et son état interne est reconstructible depuis quelques sorties.

Au tableau

« Un générateur prévisible rend le secret prévisible, donc il faut une source cryptographique, donc crypto.randomBytes. »

QUne ressource existe mais appartient à quelqu'un d'autre : 403 ou 404 ?

404. Un 403 confirme l'existence : en itérant sur des identifiants, l'appelant énumère ce qui existe. Quand la portée d'accès est dans la requête (WHERE owner_id = ?), le 404 n'est pas un mensonge : pour cet utilisateur, la ressource n'existe pas.

Au tableau

« Répondre 403 révèle qu'il y a quelque chose à protéger, donc on ne distingue pas absence et interdiction, donc 404 dans les deux cas. »

Réactivation, pas découverte. Cartes sources : tags web::*, programming::javascript, typescript, react, npm, db::pagination. Fiche jumelle : fiche 10 (backend & architecture).
À la craie : réponds à voix haute, avec les « donc », avant de retourner la fiche.
Ta réponse orale, comparée à la fiche :

Le tronc et les branches

qui a tort — le client ou le serveur — et qu'est-ce que la réponse révèle ?HTTP4xx client, 5xx serveur muet401 qui ? 403 pas le droitDELETE rejoué → succèsmiddleware avant routingJS / TSPromise { <pending> } : await oublié?? : null/undefined seulementconst xs = [] → never[]types effacés → schéma runtimeReact / fetchcomposant sync : loading puis re-renderreact-query : dedup ≠ cache hitfetch rejette seulement le transportcredentials = same-originsécuritébcrypt / argon2 : lents, coût réglablesel : désolidarise, ne renforce pastoken 32 octets → SHA-256 suffit404 pour une ressource d'autrui

Quatre branches : la sémantique HTTP (codes, idempotence, middleware, REST comme style), les pièges du runtime JS/TS (promesses, ??, never[], types effacés, npm), React et le fetch (rendu synchrone, dédup, credentials), et la sécurité des secrets (bcrypt vs SHA, sel, tokens, 403 vs 404, pagination avant filtre).