Un formulaire AJAX attend du JSON mais reçoit du HTML : remonter la réponse étape par étape
Une réponse 200 ne prouve pas qu’il s’agit de JSON. Parcourez le panneau Réseau étape par étape et séparez les pannes de transport, de représentation et de contrat.
Le formulaire est envoyé, l’indicateur apparaît et la console signale un jeton < inattendu dans JSON à la position 0. La page elle-même se charge bien, et c’est pourquoi le premier réflexe est d’accuser l’analyseur ou le tunnel. En général ni l’un ni l’autre n’est en cause : l’envoi est une requête différente du chargement de la page, et il est revenu avec quelque chose qui n’est pas du JSON. Travaillez dans l’ordre où le navigateur le révèle réellement, car les quatre réponses possibles demandent quatre corrections différentes.
Étape 1 : capturer l’envoi qui a vraiment eu lieu
Ouvrez le panneau Réseau avant d’envoyer, réglez le journal pour conserver les entrées d’une navigation à l’autre, puis faites un envoi de test inoffensif sur un compte que vous êtes autorisé à utiliser. Sélectionnez la requête déclenchée par le formulaire lui-même, et non la requête de document de la page.
Méfiez-vous des filtres. Un filtre XHR ou Fetch est pratique, mais si le framework envoie encore via une iframe cachée ou un formulaire créé dynamiquement, le filtre masquera la seule requête qui compte. Envoyez d’abord une fois sans filtre et regardez ce qui est apparu.
Pour la requête retenue, notez six choses avant toute interprétation : la méthode, l’URL complète avec la chaîne de requête, le statut, la chaîne de redirections, le Content-Type de la réponse et les premières lignes du corps. La référence du panneau Réseau de Chrome DevTools couvre les vues Headers, Response et Initiator où se trouvent ces éléments, et les panneaux équivalents des autres navigateurs affichent les mêmes champs.
Étape 2 : confirmer que c’est bien la requête attendue
Un nombre surprenant d’erreurs JSON sont en réalité des erreurs de routage. Si le gestionnaire d’envoi lève une exception avant d’appeler preventDefault, le navigateur effectue un POST natif du formulaire vers l’adresse indiquée par l’attribut action, et le serveur répond par une page HTML complète. Le gestionnaire ne s’exécute jamais, donc aucune requête JSON n’a été faite.
Comparez l’URL du panneau avec celle que votre JavaScript appelle. Cherchez une redirection qui aboutit ailleurs, une barre oblique finale manquante ou en trop, un décalage de méthode où le gestionnaire envoie GET alors que le point de terminaison accepte POST, ou un formulaire qui poste vers un chemin relatif depuis une page qui a déménagé. Suivez la chaîne de redirections jusqu’au bout et notez l’URL finale, pas la première.
Vérifiez ensuite l’initiateur. Si la requête vient d’une navigation de document et non de votre script, vous avez déjà trouvé le problème, et aucun changement d’analyseur de réponse n’aidera.
Étape 3 : classer le résultat en quatre catégories
Chaque résultat appartient à exactement une de ces catégories, et la catégorie décide où regarder ensuite.
| What you see | Category | Where to look |
|---|---|---|
| Request never finishes, TLS or connection error | No usable response | Endpoint reachability, certificate, front-end timeout |
| Request completes, script gets an opaque failure | Access blocked | Cross-origin policy, credentials mode, missing response header |
| 200 with an HTML body or a login form | Wrong representation | Redirect, expired session, route fallback, error handler |
| Body begins with a notice then valid JSON | Wrong representation | Server-side warning output before serialisation |
| Valid JSON, framework rejects it | Contract mismatch | Envelope shape, field names, value types |
Les lignes du milieu sont celles que l’on diagnostique mal, parce que le statut est 200. Une page de connexion atteinte après une redirection est une réponse HTTP parfaitement valable et un très mauvais document JSON. Le type de contenu application/json n’y remédie pas : c’est une étiquette, et un corps mal étiqueté échoue quand même à s’analyser. Accès bloqué est la ligne qui ressemble à une requête aboutie : le panneau affiche un statut, mais la politique d’origine croisée retient le corps, donc pour le script appelant elle est aussi inutilisable qu’un délai dépassé.
La quatrième ligne est tout aussi courante sur les piles PHP et les frameworks plus anciens. Un avertissement de dépréciation imprimé avant la charge encodée produit un corps qu’aucun analyseur JSON n’acceptera, et cet avertissement est souvent toute l’explication.
Étape 4 : lire l’erreur de l’analyseur au pied de la lettre
Le texte de l’erreur nomme le premier caractère qui a cassé l’analyse, ce qui identifie généralement la catégorie sans aucune conjecture.
| Error text (Chrome/V8) | What it means |
|---|---|
| Unexpected token < in JSON at position 0 | The body starts with markup: an HTML page or an error document |
| Unexpected end of JSON input | Empty body, or the response was truncated |
| Unexpected token o in JSON at position 1 | You parsed something already parsed, typically an object rather than text |
| Unexpected token u | The body starts with the bare word undefined, usually an empty response rendered as a string |
Un corps vide avec un 200 est un cas à part : le point de terminaison s’est exécuté, n’a rien produit, et quelque chose en aval l’a pris pour un succès. C’est une défaillance de contrat applicatif et non de transport, et sa place est à l’étape suivante, pas dans le proxy. Firefox et Safari formulent autrement les mêmes quatre défaillances, donc comparez sur le caractère ou sur le corps vide plutôt que sur la chaîne exacte.
Étape 5 : corriger la couche que les preuves désignent
Pour un problème de routage, corrigez la requête : rectifiez l’action, empêchez le gestionnaire de tomber dans le vide, alignez le point de terminaison et la méthode, puis retestez. Pour un problème de représentation, corrigez ce qui a produit le corps et cherchez pourquoi un document d’erreur revient là où des données étaient attendues. Pour un problème de contrat, lisez la structure réellement attendue par le framework pour la version que vous exécutez et conformez la réponse à celle-ci au lieu de relâcher le client.
Une réponse dont l’accès est bloqué demande sa propre passe, car la requête elle-même peut être correcte et le corps peut ne jamais atteindre votre code. Comparez l’Access-Control-Allow-Origin de la réponse avec l’Origin exact envoyé par le navigateur, et rappelez-vous qu’un joker ne satisfait pas une requête avec identifiants. Associez credentials: 'include' côté client à Access-Control-Allow-Credentials: true côté serveur, et vérifiez si l’appel a utilisé le mode no-cors, où un corps opaque est le résultat documenté et non un défaut.
Remplacer l’analyseur par quelque chose de permissif est la seule correction à éviter. Évaluer la réponse comme du code, ou retirer du texte jusqu’à ce qu’elle s’analyse, masque l’écart de contrat et ajoute une surface d’injection en échange du silence. De même, vérifier le statut séparément du corps est indispensable : un 200 portant un objet d’erreur applicatif est normal dans beaucoup d’API et doit être traité en lisant la charge.
Quand vous avez besoin d’aide, partagez un relevé minimal — méthode, URL, statut, URL finale après redirections, type de contenu de la réponse, forme du corps et contrat attendu — plutôt qu’un export brut. Les archives réseau contiennent des cookies, des jetons, des chaînes de requête et les données personnelles qui étaient dans le formulaire, et un assainisseur automatique n’est pas une relecture.
- Capturez la requête sans filtre avant de changer le moindre code.
- Confirmez que l’initiateur était votre script et que l’URL finale est votre point de terminaison.
- Classez la réponse avant de toucher à l’analyseur.
- Corrigez le routage, l’accès, la représentation ou le contrat, et jamais en relâchant l’analyseur.