Commit b7b2546b authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Le filet ne voyait pas la panne, seulement le refus

P11.4 du cahier : couper le réseau, écrire dans un salon. Le texte restait dans
la boîte et rien ne s'affichait.

Le filet d'erreurs ne reconnaît que les ErreurApi. Or « fetch » ne rejette pas
avec une ErreurApi : sur une panne de réseau il lève un TypeError. Le filet
couvrait donc les refus du serveur et laissait passer les pannes — c'est-à-dire
exactement le cas pour lequel ce produit existe.

Je l'avais éprouvé, en le posant, avec un refus HTTP à 403 puis à 409. Jamais
avec le réseau coupé. Deux vérifications qui se ressemblent ne font pas deux
vérifications.

── La normalisation va dans le client d'API, pas dans le filet

C'est ce que son en-tête annonce depuis l'origine : « ce fichier ne fait que
transporter des requêtes et normaliser les erreurs ». Une panne devient donc une
ErreurApi de statut zéro — aucun serveur n'en renvoie, et le zéro dit ce qui
s'est passé : aucune réponse n'est arrivée.

Le message est une clé, marquée par « traduisible » puisqu'il naît hors de tout
composant, et le filet traduit désormais ce qu'il affiche : un message venu du
serveur arrive déjà dans la bonne langue et ne correspond à aucune clé, celui
que le client fabrique en est une.

── La garantie va avec

Elle est de comportement, comme les six du garde anti-double-envoi : le module
est chargé, « fetch » remplacé par un rejet, et l'on vérifie que ce qui remonte
est une ErreurApi de statut zéro dont le message parle de réseau.

Éprouvé à l'écran après reconstruction : le bandeau dit « Le réseau n'a pas
répondu. Rien n'est parti — réessayez. », porte « role=alert », et le texte
reste dans la boîte de saisie.

── Le reste de P11

Écriture refusée par le serveur : bandeau, position fixe en bas à gauche, et le
sélecteur revient à sa valeur d'avant. Flux temps réel : reconnecté de lui-même
après la reconstruction du conteneur, « 2 en ligne » dans la barre, aucune
erreur en console.

Reste 11.6 — arrêter PostgreSQL et recharger — que je n'ai pas lancé : cela
interrompt un service de votre instance, et je préfère le demander.

Co-Authored-By: Claude (RCA)
parent 46717c59
Loading
Loading
Loading
Loading
Loading
+21 −0
Changes for apps/api/src/verification/interface.ts: 21 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -480,6 +480,27 @@ async function verifierGardeEcriture(r: Rapport): Promise<void> {
    repondre();
    await reprise;

    // Une panne de réseau se présente comme un refus, et non comme un
    // TypeError : le filet d'erreurs de la coque ne reconnaît que les
    // ErreurApi, et sans cette normalisation, couper le réseau puis écrire ne
    // produisait aucun message. Trouvé par la campagne de test, le filet
    // n'ayant jamais été éprouvé qu'avec un refus HTTP.
    repartir();
    globalThis.fetch = (() => Promise.reject(new TypeError("Failed to fetch"))) as
      unknown as typeof fetch;
    const panne = await api.poster("/api/essai", { valeur: 42 })
      .then(() => null, (e: unknown) => e);
    r.verifie("EX-19", "une panne de réseau se présente comme un refus lisible",
      panne !== null && typeof panne === "object"
      && (panne as { name?: string }).name === "ErreurApi"
      && (panne as { statut?: number }).statut === 0
      && /réseau/i.test((panne as { message?: string }).message ?? ""),
      panne instanceof Error ? `${panne.name} : ${panne.message}` : String(panne));
    globalThis.fetch = (() => new Promise<Response>((resoudre) => {
      appels += 1;
      attentes.push(() => resoudre(new Response(null, { status: 204 })));
    })) as unknown as typeof fetch;

    // Les lectures ne passent pas par le garde : react-query les regroupe déjà,
    // et deux écrans qui lisent la même chose doivent pouvoir le faire.
    repartir();
+24 −1
Changes for apps/web/src/api.ts: 24 added lines, 1 removed line.
Original line number Diff line number Diff line
import { traduisible } from "./langues/index.js";

/**
 * Client de l'API.
 *
@@ -54,13 +56,34 @@ async function requete<T>(
    charge = JSON.stringify(corps);
  }

  const reponse = await fetch(chemin, {
  /*
   * Une panne de réseau devient une ErreurApi, comme un refus.
   *
   * « fetch » ne rejette pas avec une ErreurApi mais avec un TypeError, et le
   * filet d'erreurs de la coque ne reconnaissait que les ErreurApi : couper le
   * réseau puis écrire dans un salon ne produisait donc aucun message. Le
   * texte restait dans la boîte, et rien ne disait qu'il n'était pas parti.
   *
   * Trouvé en appliquant le cahier de test — j'avais éprouvé le filet avec un
   * refus HTTP, jamais avec le réseau coupé, c'est-à-dire le cas pour lequel ce
   * produit existe.
   *
   * Le statut zéro dit ce qui s'est passé : aucune réponse n'est arrivée. Ce
   * n'est pas un code HTTP, et aucun serveur n'en renvoie.
   */
  let reponse: Response;
  try {
    reponse = await fetch(chemin, {
      ...options,
      method: methode,
      credentials: "same-origin",
      headers: entetes,
      ...(charge !== undefined ? { body: charge } : {}),
    });
  } catch {
    throw new ErreurApi(0, "reseau_injoignable",
      traduisible("Le réseau n'a pas répondu. Rien n'est parti — réessayez."));
  }

  const type = reponse.headers.get("content-type") ?? "";
  if (!reponse.ok) {
+4 −1
Changes for apps/web/src/coque/FiletEchec.tsx: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -46,7 +46,10 @@ export function FiletEchec() {
  if (echec === null) return null;
  return (
    <div className="filet-echec" role="alert">
      <span>{echec}</span>
      {/* Traduit à l'affichage : le message du serveur arrive déjà dans la
          langue demandée et ne correspondra à aucune clé, tandis que celui
          que le client fabrique — une panne de réseau — en est une. */}
      <span>{t(echec)}</span>
      <button className="discret petit" aria-label={t("Fermer l’avertissement")}
        onClick={() => definirEchec(null)}>✕</button>
    </div>
+2 −0
Changes for apps/web/src/langues/en.ts: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2266,4 +2266,6 @@ export const EN: Record<string, string> = {
    "No sheet targets “{type}” in the frozen dossier. That is the kind of gap you see once and close in calm conditions.",
  "Rectifiée par l’entrée #{seq} — conservée telle quelle.":
    "Corrected by entry #{seq} — kept as written.",
  "Le réseau n'a pas répondu. Rien n'est parti — réessayez.":
    "The network did not answer. Nothing was sent — try again.",
};