Pourquoi garder stdout MCP propre protège-t-il la frontière du protocole ?
Une gestion propre de stdout MCP empêche les journaux, bannières et trames mal formées de franchir la frontière d'une passerelle agent. Construisez un banc de test qui prouve que les appels s'arrêtent.

Une frontière MCP stdio ne reste fiable que si chaque octet de stdout appartient au protocole et si chaque requête mal formée ou ambiguë est arrêtée avant d'atteindre un exécuteur d'actions. Un analyseur qui rejette le bruit, mais laisse une requête partiellement comprise atteindre HTTP ou SSH, a échoué au point le plus dangereux.
Les équipes traitent souvent le bruit de stdout comme un simple problème d'interopérabilité agaçant. C'est trop indulgent. Lorsqu'un agent peut déclencher des actions externes, une bannière supplémentaire peut désynchroniser la conversation, dissimuler une réponse d'erreur ou amener un client tolérant à associer la mauvaise réponse à la mauvaise requête. Un bon test ne vérifie pas seulement que le processus se termine proprement. Il prouve qu'une entrée réseau mal formée ne produit aucun effet externe.
Le contrat stdout accepte un message JSON-RPC par ligne
Le transport stdio du Model Context Protocol exige des messages JSON-RPC sur stdout, séparés par des sauts de ligne, sans aucune sortie étrangère mélangée à ce flux. La règle s'applique dans l'autre sens : le client envoie les messages du protocole sur stdin et n'utilise pas ce canal comme un flux de journaux.
Cela paraît évident jusqu'à ce qu'un installateur de paquet affiche une notice, qu'une dépendance émette un avertissement ou que quelqu'un ajoute temporairement un print() dans le chemin de démarrage. Sur un terminal utilisé par une personne, ces lignes sont inoffensives. Sur un flux de protocole, ce sont des octets que le pair doit interpréter. Il ne peut pas savoir de façon sûre si loading credentials est une bannière, un résultat mal formé, un fragment de réponse ou le début d'un échange compromis.
Le contrat de transport comporte trois éléments que les tests doivent énoncer explicitement :
- Chaque ligne complète doit être décodée en UTF-8 et analysée comme une seule valeur JSON.
- Cette valeur doit avoir la forme autorisée d'une requête, d'une réponse ou d'une notification JSON-RPC pour le sens de transmission concerné.
- Aucune action ne doit commencer tant que la requête complète n'a pas franchi les contrôles de tramage, de JSON, de protocole, de schéma et d'autorisation.
La première condition détecte le bruit. La deuxième détecte un objet qui est bien un JSON valide, mais pas un message MCP. La troisième protège contre l'erreur qui compte vraiment : confondre l'analyse préliminaire avec une autorisation d'exécuter.
La spécification JSON-RPC 2.0 distingue l'erreur d'analyse de la requête invalide. Un JSON invalide peut recevoir le code d'erreur -32700 si le pair peut encore écrire une réponse correctement encadrée. Une valeur JSON dont la structure de requête est incorrecte correspond à une requête invalide, généralement -32600. Ces codes aident un pair conforme à diagnostiquer l'échec. Ils ne vous disent pas si votre propre exécuteur est resté intact. Vos tests doivent répondre directement à cette question.
Une bannière peut corrompre une réponse autorisée
Une bannière de démarrage peut interrompre une action parfaitement légitime après la réussite de l'autorisation. C'est pourquoi la propreté de stdout ne concerne pas uniquement la validation des entrées.
Imaginez un serveur qui a accepté une requête d'initialisation et s'apprête à renvoyer le résultat d'un outil. Une dépendance écrit warning: configuration missing sur stdout entre l'ouverture et la fermeture du cycle de vie de la réponse. Un client strict rejette la ligne et se déconnecte. Un client tolérant l'ignore et continue. Le client strict perd en disponibilité. Le client tolérant dispose maintenant d'une politique d'analyse qui accepte des octets sans propriétaire au sein d'un échange sensible pour la sécurité.
Ne récompensez pas le client tolérant parce qu'il semble pratique. Dès qu'un client ignore des lignes arbitraires, il se retrouve face à une longue liste de questions auxquelles il ne peut pas répondre de manière fiable. A-t-il ignoré un diagnostic ? A-t-il ignoré la réponse d'une autre requête ? Un wrapper a-t-il dupliqué une ligne ? Un attaquant capable d'influencer le processus enfant a-t-il injecté un texte qui modifie l'état du client ? Un client ne peut pas déduire l'intention d'une suite d'octets parasite.
Gardez les diagnostics sur stderr. Donnez à stderr ses propres règles de capture, de conservation et de masquage, puis faites en sorte que la supervision des processus préserve la séparation. Un problème étonnamment courant, qui ne se manifeste qu'à la publication, vient d'un wrapper qui fusionne les deux flux parce que l'affichage du terminal semblait plus agréable pendant le développement. Ce wrapper détruit discrètement la frontière du protocole.
Testez le trafic sortant sous forme d'octets bruts avant toute normalisation par une bibliothèque cliente. Si une bibliothèque transforme une séquence invalide en exception et masque la transcription originale, conservez cette transcription dans la sortie du test en échec. Voir exactement la première ligne incorrecte évite des heures de suppositions.
Le refus doit atteindre l'exécuteur d'actions
Une trame rejetée n'est sûre que si le code qui réalise une action externe ne la voit jamais. Renvoyer une réponse d'erreur est utile, mais ce n'est pas la propriété de sécurité recherchée.
Placez un enregistreur d'actions juste derrière la dernière frontière de validation et d'autorisation. Dans une véritable implémentation, cette jonction peut envelopper la fonction qui ouvre une connexion HTTP ou appelle un assistant SSH. Dans un test, utilisez un enregistreur en mémoire ou un service simulé local. Ne dirigez jamais les tests d'entrées mal formées vers un véritable point d'accès en espérant que le chemin d'erreur vous protégera.
La distinction est facile à brouiller parce qu'une requête normale suit un long parcours. Elle arrive sous forme d'octets, devient du JSON, puis un objet JSON-RPC, ensuite un appel de méthode MCP, est associé à un schéma d'outil, reçoit une décision d'autorisation et devient enfin une action. Un développeur peut ajouter un enregistrement d'audit ou construire un objet de requête avant la fin de tous ces contrôles. Cela reste acceptable uniquement si aucune de ces opérations ne peut contacter le monde extérieur ni consommer une capacité.
Un invariant utile est le suivant : l'exécuteur accepte un objet d'action entièrement typé et autorisé, jamais du JSON brut ni une requête partiellement validée. Si l'exécuteur accepte un dictionnaire générique, quelqu'un finira par l'appeler trop tôt. Le code peut réussir les tests du parcours nominal pendant des mois, car les trames mal formées sont rares dans le développement courant.
Séparez le comptage des rejets du comptage des exécutions. Un test doit pouvoir dire qu'un analyseur a rejeté une trame, que la session s'est fermée et que l'exécuteur a reçu zéro appel. Si ces événements partagent un seul compteur général de réussite ou d'échec, vous ne pouvez pas distinguer un refus propre d'une action qui a commencé avant d'échouer plus tard.
Placez la jonction de test sous l'analyse et au-dessus de l'exécution
Le plus petit banc de test utile comprend un validateur réseau strict et un exécuteur simulé. Le validateur gère les octets et la forme du protocole. L'exécuteur simulé enregistre chaque tentative d'effectuer un travail. L'adaptateur de production peut être différent, mais le contrat qui les relie doit rester étroit.
Cet exemple Python est volontairement court. Enregistrez-le sous test_stdio_boundary.py, installez pytest et lancez pytest -q test_stdio_boundary.py. Remplacez Gateway par un adaptateur vers votre propre passerelle, tout en conservant les assertions autour de Recorder.
import io
import json
import pytest
class Recorder:
def __init__(self):
self.calls = []
def execute(self, action):
self.calls.append(action)
return {'ok': True}
class Gateway:
def __init__(self, executor):
self.executor = executor
self.closed = False
def reject(self, code, reason):
self.closed = True
return {'jsonrpc': '2.0', 'id': None,
'error': {'code': code, 'message': reason}}
def receive_line(self, raw_line):
if self.closed:
return None
try:
message = json.loads(raw_line)
except json.JSONDecodeError:
return self.reject(-32700, 'parse error')
if not isinstance(message, dict):
return self.reject(-32600, 'invalid request')
if message.get('jsonrpc') != '2.0':
return self.reject(-32600, 'invalid request')
if message.get('method') != 'tools/call':
return self.reject(-32601, 'method not found')
if not isinstance(message.get('id'), (str, int)) or isinstance(message.get('id'), bool):
return self.reject(-32600, 'invalid request')
params = message.get('params')
if not isinstance(params, dict):
return self.reject(-32602, 'invalid params')
if not isinstance(params.get('name'), str):
return self.reject(-32602, 'invalid params')
if not isinstance(params.get('arguments', {}), dict):
return self.reject(-32602, 'invalid params')
action = {'name': params['name'], 'arguments': params.get('arguments', {})}
result = self.executor.execute(action)
return {'jsonrpc': '2.0', 'id': message['id'], 'result': result}
def frame(value):
return json.dumps(value, separators=(',', ':')) + '\n'
def test_bad_lines_never_execute():
bad_lines = [
'debug: entering tool handler\n',
'<html>gateway unavailable</html>\n',
'{not json}\n',
'null\n',
frame({'jsonrpc': '1.0', 'id': 4, 'method': 'tools/call', 'params': {}}),
frame({'jsonrpc': '2.0', 'id': 4, 'method': 'tools/call', 'params': 'run'}),
]
for raw_line in bad_lines:
recorder = Recorder()
gateway = Gateway(recorder)
response = gateway.receive_line(raw_line)
assert response['jsonrpc'] == '2.0'
assert 'error' in response
assert gateway.closed
assert recorder.calls == []
def test_complete_valid_request_executes_once():
recorder = Recorder()
gateway = Gateway(recorder)
request = frame({
'jsonrpc': '2.0',
'id': 9,
'method': 'tools/call',
'params': {'name': 'safe-test', 'arguments': {'value': 'green'}},
})
response = gateway.receive_line(request)
assert response['result'] == {'ok': True}
assert recorder.calls == [{'name': 'safe-test', 'arguments': {'value': 'green'}}]
Il ne s'agit pas d'une implémentation MCP complète, et cela ne doit pas le devenir. Son objectif est de rendre testable la propriété d'absence d'action. Votre adaptateur peut injecter de vrais octets stdin dans une jonction de test du même type et utiliser le véritable validateur de requêtes. Ne copiez pas la gestion abrégée des méthodes dans un serveur de production.
Notez le choix de fermer la session après une ligne entrante mal formée ou invalide. Le protocole n'impose pas cette politique de session à chaque implémentation. C'est une valeur par défaut raisonnable lorsqu'une passerelle contrôle des effets externes, car une ligne incorrecte peut signifier que le pair a perdu le cadrage. Si vous gardez la session ouverte après une requête invalide mais correctement encadrée, testez ce chemin séparément et prouvez que la requête valide suivante ne peut hériter d'aucun état de la requête rejetée.
Un banc de test exécutable doit observer les octets et les effets
Le test unitaire ci-dessus vérifie le refus des entrées. Ajoutez un test au niveau du processus pour détecter les sorties qui apparaissent en dehors du chemin de code que vous exercez habituellement. L'assistant suivant vérifie une transcription stdout capturée sans demander à une bibliothèque cliente de l'analyser à votre place.
import json
import subprocess
def assert_protocol_stdout(data):
assert data.endswith(b'\n'), 'stdout ended without a complete frame'
for number, raw_line in enumerate(data.splitlines(), start=1):
try:
line = raw_line.decode('utf-8')
message = json.loads(line)
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise AssertionError(
f'non-protocol stdout on line {number}: {raw_line!r}'
) from error
assert isinstance(message, dict), f'line {number} is not an object'
assert message.get('jsonrpc') == '2.0', f'line {number} lacks JSON-RPC 2.0'
is_notification = isinstance(message.get('method'), str) and 'id' not in message
is_response = 'id' in message and ('result' in message or 'error' in message)
assert is_notification or is_response, f'line {number} has no permitted shape'
def run_server(command, stdin_bytes):
completed = subprocess.run(
command,
input=stdin_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
assert_protocol_stdout(completed.stdout)
return completed
def test_release_command_has_clean_stdout():
initialize = (
b'{"jsonrpc":"2.0","id":1,"method":"initialize",'
b'"params":{"protocolVersion":"2025-03-26",'
b'"capabilities":{},"clientInfo":{"name":"boundary-test","version":"1"}}}\n'
)
completed = run_server(['./gateway-under-test'], initialize)
assert completed.returncode == 0
Utilisez votre véritable commande de lancement à la place de ./gateway-under-test. Si elle nécessite un environnement d'exécution, un wrapper de paquet ou des variables d'environnement, utilisez exactement les conditions de production. Le test doit inspecter stdout même lorsque le processus se termine avec un état différent de zéro. Un processus en échec peut encore émettre une bannière interdite avant de s'arrêter.
L'échec doit afficher la ligne brute en octets. Voici un résultat utile :
AssertionError: non-protocol stdout on line 1: b'loading optional extension\n'
Ce résultat indique au responsable où chercher. Un message générique comme « le serveur ne s'est pas initialisé » envoie les équipes chercher dans le code du protocole alors que le défaut peut se trouver dans un script shell, une configuration de journalisation ou une importation de dépendance.
Lancez le banc de test sous sa forme empaquetée, pas seulement depuis une copie du code source. L'empaquetage modifie la résolution des modules, les variables d'environnement, les permissions d'exécution et les chemins d'erreur. Ce sont précisément les endroits où des écritures stdout accidentelles apparaissent.
Testez les entrées difficiles plutôt que des erreurs polies
Une suite de tests de frontière doit utiliser des entrées qui ressemblent aux façons dont les vrais flux de processus échouent, pas seulement des objets invalides rédigés à la main. Faites passer chaque cas par le même point d'entrée que celui qui gère stdin et vérifiez que l'exécuteur reste vide.
Utilisez au moins les cas suivants :
- Une ligne de débogage ordinaire avant une requête valide, suivie de la requête valide à la ligne suivante.
- Une requête valide suivie d'une bannière sur la même ligne, sans séparateur de saut de ligne.
- Un objet JSON tronqué suivi de la fin du fichier.
- Deux objets JSON complets concaténés sans séparateur.
- Un objet JSON valide avec une méthode plausible, mais des paramètres mal formés.
Le premier cas détecte une implémentation qui ignore silencieusement les mauvaises lignes et poursuit son exécution. Le deuxième détecte les erreurs de tramage qu'un lecteur ligne par ligne peut prendre pour une seule requête corrompue. Le cas tronqué détecte le code qui tente de réparer l'entrée avec un tampon après la disparition du pair. Le cas des objets concaténés détecte les analyseurs configurés pour accepter plusieurs valeurs JSON de niveau supérieur alors que le transport autorise une trame par ligne.
Ne réduisez pas cette suite à un corpus pour analyseur. Associez chaque entrée incorrecte à un nom d'action simulée unique et vérifiez qu'aucun de ces noms n'apparaît dans l'enregistreur. Ajoutez ensuite une requête valide après une requête rejetée uniquement si votre politique documentée autorise la session à rester ouverte. Si votre politique ferme la session, vérifiez que la deuxième requête ne produit aucune action parce que la session est déjà fermée.
Testez aussi des données valides mais dangereuses dans leur contexte. Une requête dont method vaut tools/call et dont params est une chaîne est un JSON valide, mais ce n'est pas un appel. Une requête avec un identifiant numérique que votre langage traite comme un booléen a franchi une limite de type que vous n'aviez pas prévue. Une requête avec des arguments inconnus peut être rejetée par le schéma, mais une fusion permissive d'objets peut les transmettre accidentellement à un générateur de commandes en aval.
Le bruit au démarrage a des causes ordinaires
La plupart des contaminations de stdout viennent d'opérations de maintenance normales, pas d'une volonté de contourner le protocole. Cela ne diminue pas la nécessité de les détecter.
Un wrapper de commande peut afficher une notice de version. Un environnement modifié peut amener un environnement d'exécution à écrire un avertissement de dépréciation. Un développeur peut avoir laissé une instruction de débogage dans une importation de paquet que les tests ne chargent pas. Un gestionnaire de plantage peut afficher un message convivial sur stdout parce qu'il a été conçu pour une application de terminal. Un superviseur peut fusionner stderr et stdout pour collecter les journaux.
Traitez chaque source comme un cas de test de publication. Définissez les variables d'environnement qui activent la sortie détaillée des dépendances. Lancez l'exécutable depuis un répertoire dépourvu de sa configuration facultative. Provoquez une erreur de démarrage récupérable. Exercez la première requête après un démarrage sans activité. Vérifiez ensuite la transcription brute à chaque fois.
Ne réduisez pas tous les journaux au silence pour faire passer le test. Cela ne ferait que déplacer le problème opérationnel. Acheminez les diagnostics vers stderr, donnez aux opérateurs un moyen explicite de les collecter et masquez les valeurs sensibles avant qu'elles ne quittent le processus. La conformité du protocole et des diagnostics utiles peuvent coexister lorsque les flux restent séparés.
Un JSON valide ne prouve pas qu'une requête est sûre
Un analyseur JSON strict protège le tramage. Il ne décide pas si une requête peut utiliser des identifiants ou atteindre un hôte.
Gardez ces décisions dans cet ordre. D'abord, le transport lit une trame. Ensuite, l'analyseur produit une valeur JSON. Puis le validateur du protocole établit la forme JSON-RPC et MCP de la méthode. Le validateur de schéma vérifie les arguments de l'outil. Ce n'est qu'après ces étapes que l'autorisation doit décider si l'action demandée peut s'exécuter. L'exécuteur doit recevoir une action typée accompagnée de cette décision, pas un nom de méthode brut et un dictionnaire de paramètres.
Cet ordre évite un échec subtil : construire une requête HTTP alors que la validation est encore en cours. Si un contrôle ultérieur rejette l'appel, mais qu'une bibliothèque a déjà résolu un hôte, ouvert une connexion ou développé un modèle de commande, votre test peut signaler un refus alors que la frontière a déjà laissé passer une opération. Le test avec exécuteur simulé détecte la forme évidente de ce problème. Un service HTTP local simulé ou un assistant de test SSH peut détecter une activité réseau accidentelle dans les tests d'intégration.
Une requête rejetée peut avoir sa place dans un enregistrement d'audit, mais ne transformez pas cet enregistrement en canal secondaire qui appelle la couche d'action. Enregistrez le rejet comme un rejet, avec une raison et une empreinte de requête qui ne peut pas révéler de secrets. Gardez le journal des actions externes distinct d'un appel tenté qui n'a jamais franchi l'autorisation.
Une seule instruction print accidentelle peut dissimuler un échec dangereux
Imaginez une passerelle qui prend en charge un outil nommé deploy-preview. Son gestionnaire valide certains arguments, commence à préparer une requête sortante, puis vérifie si l'appelant peut utiliser l'identifiant sélectionné. Lors d'une refactorisation, un développeur ajoute une instruction print sur stdout pour examiner la cible sélectionnée.
Un client MCP strict voit cette sortie, ne peut pas l'analyser comme du JSON-RPC et se déconnecte. Le développeur constate un échec du protocole et corrige l'instruction print. C'est gênant, mais sûr.
Un client tolérant ignore la ligne, reçoit une réponse d'erreur et indique à l'opérateur que l'autorisation a refusé la requête. Pourtant, le gestionnaire avait déjà transmis la requête assemblée à un assistant HTTP avec nouvelle tentative avant le contrôle d'autorisation. Le service distant reçoit la requête sans identifiant exploitable, renvoie peut-être une erreur et laisse à l'équipe une piste d'audit trompeuse : l'agent semble avoir été refusé, alors que le service a tout de même observé une activité.
La bannière n'a pas créé le problème d'ordre des autorisations. Elle a révélé pourquoi la récupération tolérante du transport et la construction précoce d'une action forment un mauvais couple. La solution n'est pas une règle d'ignorance plus intelligente. Placez la décision d'autorisation avant la construction de la requête, utilisez l'enregistreur pour le prouver et gardez stdout si strict qu'une sortie accidentelle fait immédiatement échouer le test.
Faites du contrat réseau une condition de publication
Placez un test de stdout propre à côté de chaque commande de passerelle empaquetée et exécutez-le en intégration continue. Faites en sorte que l'échec inclue les premiers octets en cause, la commande de lancement et stderr comme pièce jointe distincte. Un responsable doit pouvoir reproduire l'échec sans reconstituer une conversation avec un agent.
Conservez sous contrôle de version un petit corpus de trames entrantes mal formées. Ajoutez un cas chaque fois qu'un défaut réel apparaît. Résistez à l'envie d'accepter une nouvelle anomalie simplement parce qu'un client l'a émise. Si une implémentation envoie du trafic mal formé, corrigez-la ou documentez une frontière de compatibilité versionnée qui n'autorise aucune action.
Sallyport place l'exécution HTTP et SSH derrière son shim stdio intégré sp mcp. Ce banc de test doit donc se trouver à la frontière de ce shim et vérifier qu'une trame rejetée ne peut pas déclencher un appel externe. La même discipline s'applique à tout serveur MCP capable de faire autre chose que renvoyer du texte.
Le critère de publication est simple : stdout ne contient que des messages complets du protocole et une entrée rejetée ne laisse aucune trace dans l'enregistreur d'actions. Si l'une ou l'autre assertion échoue, la version n'est pas prête à transporter du trafic d'agents.
FAQ
Un serveur MCP peut-il afficher un message de démarrage sur stdout ?
Non. Avec le transport stdio, stdout transporte les trames du protocole. Une bannière a donc le même statut qu'un JSON corrompu. Envoyez les diagnostics vers stderr ou vers une destination de journalisation structurée distincte.
Que doit faire une passerelle MCP avec un JSON mal formé ?
Traitez-le comme un échec de la frontière de transport et vérifiez qu'aucune action n'est parvenue à l'exécuteur. Le choix de renvoyer une erreur d'analyse JSON-RPC avant de fermer le processus dépend du produit, mais le serveur ne doit jamais deviner le sens de la ligne.
Quelle erreur JSON-RPC s'applique à un JSON invalide ?
JSON-RPC 2.0 définit le code d'erreur d'analyse -32700 pour un JSON invalide. Cette erreur décrit le problème du message. Elle ne prouve pas que votre implémentation a arrêté l'opération HTTP ou SSH en aval, d'où l'importance de vérifier directement les effets dans le test.
Un JSON valide suffit-il pour autoriser un appel d'outil MCP ?
Non. Un message peut être correctement analysé tout en contenant une mauvaise version du protocole, une méthode non prise en charge, un identifiant de requête invalide ou des arguments qui ne correspondent pas au schéma de l'action. La validation de l'analyse et l'autorisation de l'action sont deux barrières distinctes.
Dois-je tester stdio MCP avec des tests unitaires ou d'intégration ?
Exercez tout le chemin des octets dans des tests d'intégration, puis utilisez des tests unitaires pour couvrir rapidement l'analyseur et la frontière d'action. Les tests unitaires seuls ne détectent souvent pas une bibliothèque, un wrapper ou un profil shell qui écrit une bannière avant le démarrage de votre code.
Comment détecter le bruit stdout qui n'apparaît qu'en production ?
Lancez-le avec le même environnement, le même wrapper shell, le même empaquetage et la même commande de démarrage qu'en production. Un terminal local propre ne prouve pas grand-chose si la version installée ajoute une notice, un avertissement ou une ligne de progression.
stderr est-il sûr pour les journaux d'un serveur MCP ?
stderr est séparé du flux du protocole stdio, c'est donc la destination normale des journaux et des diagnostics. Gardez-le accessible aux opérateurs, mais évitez de fusionner stderr avec stdout dans les superviseurs de processus ou les exécuteurs de tests.
Que doit vérifier un banc de test stdio pour MCP ?
Il doit prouver deux choses : chaque ligne stdout correspond à une forme JSON-RPC autorisée, et chaque entrée refusée laisse l'enregistreur d'actions vide. La vérification de la transcription sans vérification des effets peut manquer le point le plus dangereux.
Un client MCP doit-il essayer de récupérer une ligne stdout inattendue ?
Faites échouer l'ambiguïté par défaut. Les erreurs d'analyse, les octets non blancs supplémentaires, les trames dupliquées lorsqu'une seule est attendue et les entrées incomplètes doivent arrêter la session ou la requête avant qu'un appel n'atteigne l'exécuteur.
Comment tester des appels MCP refusés sans risquer d'utiliser de vrais identifiants ?
Utilisez un point d'accès simulé local ou un enregistreur incapable de contacter des services réels, puis vérifiez que sa liste d'appels reste vide. Ne testez pas les entrées mal formées avec des identifiants réels simplement parce que l'action est censée être refusée.