Symptôme. Une extension personnalisée développée pour un client du secteur associatif provoquait, de façon intermittente, le message Warning: Cannot modify header information - headers already sent by (output started at ...) au moment de sauvegarder un formulaire d’adhésion. La redirection censée suivre la sauvegarde ne se produisait jamais, laissant l’utilisateur face à un écran d’erreur PHP en haut de page.
Comprendre ce que dit réellement le message
La première confusion à dissiper : le message d’erreur indique où PHP a tenté d’envoyer un en-tête HTTP en échec (généralement un appel à wp_redirect(), wp_safe_redirect() ou setcookie()), pas où la sortie parasite a commencé. La mention « output started at » donne heureusement cette seconde information, mais elle pointe souvent vers un fichier totalement éloigné du code qu’on soupçonne en premier.
Warning: Cannot modify header information - headers already sent
by (output started at /wp-content/plugins/kaolin-adhesions/kaolin-adhesions.php:1)
in /wp-includes/pluggable.php on line 1408
Ici, le numéro de ligne 1 du fichier principal de l’extension est le vrai indice : une sortie s’est produite dès la première ligne du fichier, bien avant que le code métier n’ait eu la moindre chance de s’exécuter.
Diagnostic : traquer la sortie parasite
Dans l’immense majorité des cas que j’ai rencontrés, la cause se limite à quatre possibilités, classées par fréquence décroissante.

- Un espace, une tabulation ou un retour à la ligne avant la balise ouvrante
<?phpdu fichier concerné - Une balise fermante
?>en fin de fichier, suivie d’un retour à la ligne invisible dans l’éditeur - Un caractère BOM (byte order mark) inséré silencieusement par certains éditeurs de texte lors de l’enregistrement en UTF-8
- Un appel de débogage oublié (
echo,print_r,var_dump) exécuté avant la tentative de redirection
Pour ce projet précis, la cause était la troisième : le fichier principal avait été réenregistré par un éditeur de texte configuré en « UTF-8 avec BOM » après une intervention rapide d’un collègue, ajoutant trois octets invisibles en tout début de fichier. Ces trois octets suffisent à constituer une sortie, même si aucun caractère visible n’apparaît à l’écran.
Correctif : localiser et supprimer la sortie
La commande grep en ligne de commande permet de repérer rapidement les balises fermantes suspectes dans l’ensemble des fichiers d’une extension :
grep -rn '?>$' wp-content/plugins/kaolin-adhesions/
Pour un BOM, un éditeur de texte capable d’afficher les caractères invisibles (ou une commande comme file sous Linux) permet de le confirmer :
file wp-content/plugins/kaolin-adhesions/kaolin-adhesions.php
# renvoie : ... UTF-8 Unicode (with BOM) text
Le correctif consiste à réenregistrer le fichier en UTF-8 sans BOM, une option disponible dans la quasi-totalité des éditeurs modernes, puis à supprimer toute balise fermante ?> en fin de fichier PHP — elle n’est jamais requise par l’interpréteur et sa seule utilité est de créer ce genre de bug.
// Avant, en fin de fichier :
function kaolin_traiter_adhesion() { /* ... */ }
?>
// Après :
function kaolin_traiter_adhesion() { /* ... */ }
Prévention : éviter que le problème ne revienne
Trois habitudes ont éliminé ce type d’incident pour de bon sur les projets suivants de cette agence.
- Bannir systématiquement la balise fermante
?>en fin de fichier PHP, dans tous les fichiers de l’extension - Configurer l’éditeur de l’équipe en UTF-8 sans BOM par défaut pour tous les fichiers PHP
- Ajouter une vérification automatique dans le processus d’intégration continue, via un script simple qui détecte les BOM sur l’ensemble du dépôt avant chaque déploiement
Un dépannage utile en urgence, mais à ne jamais considérer comme un correctif définitif : activer
ob_start()en tout début de fichier avale la sortie parasite et fait disparaître le symptôme, sans supprimer sa cause. Le problème resurgit inévitablement, souvent dans un contexte oùob_start()n’a pas été pensé.
En résumé
« Headers already sent » n’est jamais une erreur aléatoire : c’est toujours une sortie physique, même invisible, qui s’est produite avant l’appel qui échoue. La méthode reste constante d’un projet à l’autre : lire attentivement la mention « output started at », vérifier balise fermante, espaces parasites et BOM dans le fichier indiqué, puis remonter au code de débogage oublié si les trois premières pistes ne donnent rien.