Le problème
Si vous avez déjà vu ce message dans vos logs ou en haut d’une page WordPress, vous savez à quel point il peut être pénible à remonter :
Warning: Cannot modify header information - headers already sent by (output started at /home/site/public_html/wp-content/themes/mon-theme/functions.php:12) in /home/site/public_html/wp-includes/pluggable.php on line 1427
Sur WordPress 6.9.4 (avril 2026) et PHP 8.1+, cette erreur apparaît typiquement :
- au moment d’une connexion/déconnexion (cookies, redirections),
- lors d’un wp_redirect() après un formulaire,
- sur des endpoints REST/AJAX (réponse JSON cassée),
- après une mise à jour de thème/plugin, ou l’ajout d’un snippet (Code Snippets, mu-plugin, functions.php).
Elle peut se manifester côté front, dans l’admin, via WP-Cron, ou uniquement sur certains templates. À la fin, vous saurez :
- identifier précisément quel fichier a produit une sortie (output) trop tôt,
- corriger les cas classiques (BOM, espaces, echo/var_dump, erreurs PHP “affichées”),
- mettre en place un diagnostic reproductible (Query Monitor, Health Check, WP-CLI, logs).
Résumé rapide
- “Headers already sent” signifie : du contenu a été envoyé avant que PHP puisse envoyer des en-têtes HTTP (cookies, redirections, type de contenu).
- La cause n°1 : espaces ou BOM UTF‑8 avant
<?phpou après?>dans un fichier PHP (souventwp-config.php,functions.php, un plugin). - La cause n°2 : echo/var_dump dans un hook trop tôt (ou dans un fichier inclus), parfois via un plugin de snippets.
- Le message “output started at …:line” est votre piste : allez à ce fichier et cette ligne, puis remontez l’output (même un espace compte).
- Pour diagnostiquer vite : activez les logs (
WP_DEBUG_LOG), utilisez Query Monitor + Health Check, et testez en désactivant les plugins par lot.
Les symptômes
Selon l’endroit où WordPress tente d’envoyer des en-têtes, vous verrez des symptômes très différents. Ceux que je croise le plus :
- Impossible de se connecter (cookie non posé) ou boucle de connexion.
- Redirection cassée après soumission d’un formulaire (contact, checkout, login).
- Warning visible en haut de page + mise en page décalée (un warning “imprimé” devient du HTML).
- REST API cassée : réponse JSON invalide car un warning/texte a été injecté avant le JSON (souvent visible dans la console navigateur).
- Admin-ajax.php renvoie du HTML au lieu de JSON → blocs Gutenberg, Elementor, Divi, Avada qui ne chargent plus correctement.
- Erreur 500 si votre serveur convertit certains warnings en erreurs fatales (rare, mais vu sur des stacks très strictes).
- Ça marche en local mais pas en prod : la prod a
display_errors=On(mauvaise pratique) ou une configuration différente d’encodage/éditeur. - Uniquement après activation d’un plugin ou d’un snippet : le plugin “écrit” quelque chose (même un espace) trop tôt.
Cas typique côté page builders (Divi 5 / Elementor / Avada) : l’éditeur “tourne” indéfiniment, et dans l’onglet Réseau (Network) vous voyez une réponse qui commence par Warning: Cannot modify header information... avant le JSON.
Tableau de diagnostic rapide
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Warning “headers already sent” + chemin vers functions.php | Espaces/BOM ou echo dans le thème | Ouvrir le fichier à la ligne indiquée, afficher les caractères invisibles | Retirer BOM/espaces, supprimer ?> final, déplacer l’output |
| REST API renvoie du HTML au lieu de JSON | Warning affiché (display_errors) ou output d’un plugin | Console navigateur + réponse brute de l’endpoint | Désactiver display_errors, corriger plugin/snippet |
| Impossible de se connecter / cookies non définis | Output avant setcookie() (login) |
Logs PHP + chemin “output started at …” | Supprimer l’output en amont (BOM/echo), corriger hook |
| Erreur après ajout d’un snippet | Echo/var_dump dans mu-plugin/snippet | Désactiver le snippet (ou renommer le fichier mu-plugin) | Remplacer par error_log(), utiliser hooks adaptés |
| Ça apparaît seulement sur certaines pages | Template spécifique / shortcode qui echo | Tester template par template, Query Monitor | Corriger le shortcode (return vs echo), bufferiser proprement |
Pourquoi ça arrive
Explication simple
En HTTP, les “headers” (en-têtes) doivent être envoyés avant le contenu (HTML/JSON). En PHP, dès que quelque chose est imprimé (même un espace ou un caractère invisible), PHP considère que la réponse a commencé. Après ça, toute tentative de modifier les en-têtes échoue : cookies, redirections, type de contenu, statut HTTP.
Explication technique (WordPress 6.9.4 / PHP 8.1+)
WordPress envoie des headers à plusieurs endroits : authentification (cookies), redirections (wp_redirect()), REST API (Content-Type JSON), admin-ajax, feeds, etc. Beaucoup passent par des fonctions “pluggable” (chargées après les plugins), d’où le chemin fréquent vers wp-includes/pluggable.php dans le warning.
Le message “headers already sent by (output started at …:line)” est crucial : il ne dit pas “où WordPress a échoué”, il dit où l’output a commencé.
Causes probables (du plus fréquent au plus rare)
- Espaces, lignes vides avant
<?phpdans un fichier PHP. - BOM UTF‑8 (Byte Order Mark) ajouté par certains éditeurs, surtout lors d’un copier/coller depuis un tutoriel.
- Balise de fermeture
?>suivie d’un retour ligne/espaces (souvent dansfunctions.phpou un plugin). - echo / print / var_dump / printf exécuté trop tôt (dans un plugin, mu-plugin, snippet, ou un fichier inclus).
- Shortcode ou callback REST/AJAX qui echo au lieu de
return(JSON cassé). - Erreurs PHP affichées parce que
display_errorsest activé en production (mauvaise pratique) : un warning “s’affiche” et déclenche l’output. - Compression/caching mal configurés (rare) : certains modules/proxies déclenchent un flush prématuré.
Prérequis avant de commencer
- Sauvegarde : fichiers + base de données. Sur un site à trafic, faites-le avant toute modification de
wp-config.phpou du thème. - Environnement de test : staging si possible. J’ai souvent vu ce bug “réparé” en prod… puis réintroduit au prochain déploiement.
- Versions : WordPress 6.9.4, PHP 8.1+ (recommandé). Si vous êtes encore en PHP 7.x, corrigez d’abord ça.
- Accès : FTP/SFTP ou SSH, et idéalement WP-CLI.
Outils utiles
- Query Monitor (pour voir les erreurs, hooks, requêtes, REST/AJAX) : https://wordpress.org/plugins/query-monitor/
- Health Check & Troubleshooting (mode dépannage sans impacter les visiteurs) : https://wordpress.org/plugins/health-check/
- Documentation debug : Debugging in WordPress
Configurer des logs propres (sans casser le site)
Sur WordPress 6.9.4, je vise presque toujours : erreurs loguées, pas affichées. Dans wp-config.php (avant “That’s all, stop editing!”), utilisez :
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
/* Ne jamais afficher les erreurs en production : ça casse le JSON, les headers, et fuit des infos */
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', '0' );
Référence officielle : developer.wordpress.org – Debug WordPress. Le fichier de log se trouve généralement dans wp-content/debug.log.
Solution 1 : Traquer les espaces/UTF-8 BOM avant <?php (thème, plugin, wp-config.php)
C’est la cause la plus fréquente, et la plus frustrante : un caractère invisible suffit. Je l’ai vu après :
- un FTP en mode “texte” qui modifie des fins de ligne,
- un éditeur qui ajoute un BOM,
- un copier/coller depuis un snippet,
- un
?>final laissé dans un fichier.
Cas 1 : espaces avant <?php
AVANT (cassé) — il y a une ligne vide avant <?php :
<?php
/* functions.php du thème */
add_action( 'init', function() {
// ...
} );
Oui, cette ligne vide au tout début peut suffire si WordPress tente d’envoyer des cookies/headers ensuite.
APRÈS (corrigé) — le fichier commence immédiatement par <?php :
<?php
/* functions.php du thème */
add_action( 'init', function() {
// ...
} );
Pourquoi ça corrige
PHP envoie tout caractère “hors PHP” tel quel. Une ligne vide avant <?php est du texte. Donc output → headers verrouillés.
Cas 2 : BOM UTF‑8 (le “fantôme” le plus courant)
Le BOM est invisible dans beaucoup d’éditeurs, mais PHP le considère comme de l’output. Symptôme typique : “output started at wp-config.php:1” alors que vous ne voyez rien.
Diagnostic (SSH) : détecter un BOM au début d’un fichier.
# Affiche les 3 premiers octets en hexadécimal
head -c 3 wp-config.php | hexdump -C
Si vous voyez ef bb bf, vous avez un BOM UTF‑8.
Correction : réenregistrer le fichier en “UTF‑8 sans BOM”. Dans VS Code : “Save with Encoding” → “UTF-8”. Dans PhpStorm : File Encoding → UTF‑8 (sans BOM). Ou en ligne de commande :
# Supprime le BOM UTF-8 si présent (macOS/Linux)
sed -i '1s/^xEFxBBxBF//' wp-config.php
Référence BOM côté PHP : php.net – header() (les notes utilisateurs évoquent fréquemment les sorties prématurées, même si le BOM n’est pas “PHP” à proprement parler).
Cas 3 : balise de fermeture ?> en fin de fichier
Dans un fichier PHP “pur” (pas un template mixte), la balise ?> finale est un piège : elle autorise des espaces après la fermeture.
AVANT (cassé) :
<?php
// plugin.php
add_action( 'init', 'mon_init' );
function mon_init() {
// ...
}
?>
APRÈS (corrigé) — supprimez la fermeture :
<?php
// plugin.php
add_action( 'init', 'mon_init' );
function mon_init() {
// ...
}
/* Pas de balise de fermeture PHP : évite les espaces involontaires */
Où chercher en priorité
wp-config.php(très fréquent)functions.phpdu thème (ou thème enfant)- un fichier principal de plugin (celui qui a l’en-tête “Plugin Name”)
wp-content/mu-plugins/- un plugin de snippets (les snippets sont stockés en DB, mais le plugin qui les exécute peut aussi générer de l’output en cas d’erreur)
Solution 2 : Supprimer les echo/var_dump trop tôt (hooks, mu-plugins, snippets)
Deuxième cause très courante : quelqu’un a ajouté un debug “vite fait” (ou un message) qui s’exécute avant que WordPress n’ait fini de gérer les headers. Le pire : un var_dump() dans un hook global, ou dans un fichier inclus.
Cas 1 : echo dans un plugin au chargement
AVANT (cassé) — output immédiat dès le chargement du plugin :
<?php
/**
* Plugin Name: Mon Plugin
*/
echo 'Plugin chargé'; // ❌ Sortie immédiate : casse cookies / redirections / REST
add_action( 'init', function() {
// ...
} );
APRÈS (corrigé) — remplacez par un log, et affichez seulement dans un endroit prévu pour du HTML :
<?php
/**
* Plugin Name: Mon Plugin
*/
add_action( 'plugins_loaded', function() {
/* Log uniquement si WP_DEBUG est actif */
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( '[Mon Plugin] Plugin chargé' ); // ✅ Pas de sortie HTTP
}
} );
add_action( 'wp_footer', function() {
/* Exemple: si vous devez afficher un message, faites-le dans le HTML, pas au chargement */
if ( current_user_can( 'manage_options' ) ) {
echo '<!-- Mon Plugin actif -->';
}
} );
Pourquoi ça corrige
Les headers sont envoyés très tôt (et parfois tard, selon buffering), mais vous ne devez jamais “imprimer” quelque chose au chargement d’un plugin. error_log() écrit dans les logs serveur/PHP, pas dans la réponse HTTP.
Cas 2 : var_dump dans un hook “admin_init” ou “init”
Je le vois souvent après dépannage d’un bug : on oublie de retirer un var_dump().
AVANT (cassé) :
<?php
add_action( 'init', function() {
var_dump( $_GET ); // ❌ Casse REST/AJAX et redirections
} );
APRÈS (corrigé) :
<?php
add_action( 'init', function() {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
/* Log JSON lisible, sans polluer la sortie HTTP */
error_log( '[DEBUG] $_GET=' . wp_json_encode( $_GET ) );
}
} );
Note : wp_json_encode() est préférable à json_encode() pour gérer certains cas WordPress. Référence : developer.wordpress.org – wp_json_encode().
Cas 3 : shortcode qui echo au lieu de return
Ça ne déclenche pas toujours “headers already sent”, mais ça casse souvent l’API REST ou des réponses AJAX quand le shortcode est utilisé dans un contexte inattendu (ex. rendu dans un endpoint, dans un email, etc.). Et quand un warning s’ajoute, tout s’écroule.
AVANT (anti-pattern) :
<?php
add_shortcode( 'mon_bloc', function( $atts ) {
echo '<div class="mon-bloc">Hello</div>'; // ❌ echo dans un shortcode
} );
APRÈS (corrigé) :
<?php
add_shortcode( 'mon_bloc', function( $atts ) {
return '<div class="mon-bloc">Hello</div>'; // ✅ return
} );
Cas 4 : redirection après output (wp_redirect mal placé)
Le pattern classique : vous affichez quelque chose, puis vous redirigez. Trop tard.
AVANT (cassé) :
<?php
add_action( 'template_redirect', function() {
if ( isset( $_GET['go'] ) ) {
echo 'Redirection...'; // ❌ output
wp_redirect( home_url( '/' ) );
exit;
}
} );
APRÈS (corrigé) :
<?php
add_action( 'template_redirect', function() {
if ( isset( $_GET['go'] ) ) {
wp_safe_redirect( home_url( '/' ) ); // ✅ redirection “safe”
exit;
}
} );
Référence : developer.wordpress.org – wp_safe_redirect().
Compatibilité Divi 5 / Elementor / Avada : le piège AJAX/REST
Divi 5, Elementor et Avada s’appuient fortement sur admin-ajax et la REST API pour l’éditeur, les bibliothèques, les imports, etc. Un simple output parasite dans un hook global peut :
- ajouter du texte avant le JSON,
- transformer une réponse JSON en HTML,
- déclencher “headers already sent” si des cookies/headers sont ajoutés ensuite.
Quand vous dépannez, testez explicitement :
- l’éditeur du builder,
- un endpoint REST (ex.
/wp-json/wp/v2/types), - un appel admin-ajax lié au builder (Network tab).
Solution 3 : Diagnostic avancé (output buffering, WP-CLI, logs serveur)
Quand “output started at …” pointe vers un fichier généré, minifié, ou un plugin obfusqué, il faut des outils plus “chirurgicaux”. Ici, l’objectif n’est pas de masquer le problème, mais de trouver qui écrit.
Étape 1 : vérifier si PHP affiche des erreurs en production
Si display_errors est actif, un warning peut être envoyé avant les headers. Vérifiez :
php -i | grep -i display_errors
Sur un hébergement mutualisé, vous n’avez pas toujours la main. Dans ce cas, gardez WP_DEBUG_DISPLAY à false comme plus haut, et forcez via @ini_set (ça dépend de la config serveur).
Étape 2 : WP-CLI pour isoler plugins/thème sans casser le front
Avec WP-CLI, vous pouvez désactiver par lot. Référence : WP-CLI – plugin deactivate.
# Lister les plugins
wp plugin list
# Désactiver tous les plugins (diagnostic)
wp plugin deactivate --all
# Réactiver un par un (ou par groupe)
wp plugin activate query-monitor
wp plugin activate nom-du-plugin-suspect
Pour le thème :
# Basculer temporairement sur un thème par défaut (si présent)
wp theme list
wp theme activate twentytwentyfive
Sur des sites Divi/Elementor/Avada, je fais souvent ce test sur un staging : si l’erreur disparaît avec un thème par défaut, le problème est presque toujours dans le thème enfant ou un module custom.
Étape 3 : capturer l’output “trop tôt” (sans le laisser casser les headers)
Ce point est délicat : l’output buffering peut masquer le symptôme. Mais en diagnostic, il peut vous aider à prouver qu’un plugin écrit avant l’heure.
Approche pragmatique (temporaire) : bufferiser très tôt et logger la présence d’output avant l’exécution complète de WordPress. Ajoutez un mu-plugin uniquement sur staging :
<?php
/**
* Plugin Name: (MU) Diagnostic Output Précoce
* Description: À utiliser temporairement sur staging pour détecter une sortie avant les headers.
*/
/* Démarre un buffer très tôt */
ob_start();
add_action( 'shutdown', function() {
/* Récupère ce qui a été envoyé */
$output = ob_get_contents();
if ( $output ) {
/* On limite la taille loguée pour éviter d'exploser les logs */
$extrait = substr( $output, 0, 2000 );
error_log( '[DIAG OUTPUT] Début de sortie détecté (2000 chars max) : ' . str_replace( array("n", "r"), ' ', $extrait ) );
}
/* Laisse PHP terminer normalement */
}, PHP_INT_MAX );
Risques : sur un site très chargé, bufferiser peut augmenter la mémoire. Ne laissez pas ça en place. Le but est de capturer un extrait, pas de “corriger”.
Étape 4 : logs serveur (Nginx/Apache + PHP-FPM)
Quand WordPress ne vous donne pas assez d’infos, les logs serveur le feront :
- error_log PHP-FPM (souvent plus riche),
- error_log Nginx/Apache,
- logs de votre reverse proxy/CDN si présent.
Sur beaucoup de stacks, le warning “headers already sent” est accompagné d’une trace plus complète côté PHP-FPM.
Vérifications après correction
- Rechargez la page incriminée en navigation privée (pour éviter un cookie/session en cache).
- Videz les caches : plugin de cache, cache serveur, CDN, et cache navigateur (j’ai vu l’erreur “persistante” uniquement parce que le HTML warning était en cache).
- Testez login/logout et soumission de formulaires (redirections).
- Testez REST API : ouvrez
/wp-json/et un endpoint (ex./wp-json/wp/v2/posts?per_page=1). Vous devez obtenir du JSON propre, sans texte avant. - Testez admin-ajax via l’outil qui posait problème (builder, plugin).
- Vérifiez debug.log : l’objectif est “plus de warning headers already sent”, même si d’autres warnings PHP restent à corriger.
Si vous utilisez Query Monitor, regardez aussi le panneau “HTTP API Calls” et “AJAX” : une réponse polluée se repère vite.
Si ça ne marche toujours pas
Voici un ordre de dépannage qui évite de tourner en rond. Je l’utilise sur des sites clients quand l’erreur revient “sans raison”.
- Repérez le premier “output started at …” dans le warning. Si vous avez plusieurs warnings, partez du premier affiché/consigné.
- Ouvrez le fichier à la ligne indiquée et cherchez :
- espaces avant
<?php, - BOM,
?>final,echo,print,var_dump,dump(Symfony),- un include/require qui charge un fichier “mixte”.
- espaces avant
- Désactivez les plugins par lot (Health Check en “Troubleshooting mode” si vous ne voulez pas impacter les visiteurs). Référence : Health Check & Troubleshooting.
- Basculez sur un thème par défaut (ou désactivez le thème enfant). Beaucoup de “headers already sent” viennent d’un
functions.phpde thème enfant modifié à la main. - Désactivez temporairement le cache (plugin + cache serveur). Certains caches servent une page contenant déjà le warning, ce qui vous fait croire que la correction n’a rien fait.
- Vérifiez la console navigateur (REST/AJAX). Si vous voyez une réponse non-JSON, vous avez encore un output parasite.
- Vérifiez les permissions et déploiements : j’ai déjà vu un “fix” appliqué sur le mauvais fichier (mauvais répertoire, symlink, release non déployée).
- Vérifiez la version PHP et les modules : un warning PHP 8.1+ (dépréciations, types) peut s’afficher si votre serveur est mal configuré (display_errors).
Cas particulier : l’erreur pointe vers pluggable.php
Quand le warning mentionne wp-includes/pluggable.php, ce n’est presque jamais le coupable. C’est l’endroit où WordPress appelle setcookie(), wp_redirect(), etc. Le coupable est dans “output started at …”.
Référence (code core) : github.com/WordPress/wordpress-develop (recherchez pluggable.php dans le dépôt).
Pièges et erreurs courantes
| Symptôme | Cause probable | Solution recommandée |
|---|---|---|
| “output started at wp-config.php:1” | BOM UTF‑8 ou espace avant <?php |
Réenregistrer en UTF‑8 sans BOM, supprimer tout caractère avant <?php |
| Erreur après avoir “ajouté un code dans functions.php” | Code copié au mauvais endroit / balise ?> / espace après fermeture |
Supprimer ?> final, coller le code après <?php sans sortie |
| REST API renvoie du HTML | Warning PHP affiché (display_errors) ou echo dans un hook global | Désactiver l’affichage d’erreurs, corriger le plugin/snippet, vérifier Network |
| Ça ne se produit que pour les admins | Debug conditionnel sur current_user_can() mais avec echo trop tôt |
Remplacer par error_log() ou afficher dans admin_notices/wp_footer |
| “Ça marche puis ça recasse après vidage de cache” | Cache qui sert une page “polluée” ou minification qui injecte du contenu | Désactiver minification temporairement, purger tous les niveaux de cache |
| Erreur après mise à jour PHP 8.1+ | Dépréciations/Warnings affichés par le serveur | Corriger le code, mais surtout ne pas afficher les erreurs en production |
| Vous avez “corrigé” en ajoutant ob_start partout | Masquage du symptôme, pas de correction | Retirer le buffering “pansement”, trouver la source de l’output |
| Snippet cassé via plugin de snippets | Erreur de syntaxe (point-virgule manquant) + affichage d’erreur | Désactiver le snippet, corriger la syntaxe, loguer au lieu d’afficher |
Erreurs réalistes que je vois souvent
- Copier le code au mauvais endroit : collé après une fermeture
?>existante, donc output hors PHP. - Oublier un point-virgule : déclenche un message d’erreur affiché si
display_errorsest actif, ce qui pollue la sortie. - Utiliser un hook inadapté : ex.
initpour “afficher” un message, alors que ça doit aller dansadmin_noticesou dans un template. - Priorité de hook : un plugin “debug” en priorité 1 imprime avant que d’autres n’aient fini.
- Tester sur production sans possibilité de rollback : et rester bloqué hors admin.
Variante / alternative
Méthode sans code : mode dépannage isolé (Health Check)
Si vous ne pouvez pas vous permettre de désactiver des plugins en prod, Health Check est parfait : il vous permet de désactiver plugins et thème uniquement pour votre session.
- Installez/activez : https://wordpress.org/plugins/health-check/
- Activez “Troubleshooting mode”.
- Désactivez plugins un par un jusqu’à disparition de l’erreur.
Sur des sites Elementor/Divi/Avada, ça évite de casser l’éditeur pour tout le monde pendant votre diagnostic.
Méthode développeur : transformer les “echo de debug” en logs structurés
Si votre équipe a l’habitude de “dump”, imposez une règle simple : jamais d’output, uniquement des logs. Exemple minimal :
<?php
function mon_log_debug( $message, array $context = array() ) {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
$line = '[DEBUG] ' . $message;
if ( ! empty( $context ) ) {
$line .= ' ' . wp_json_encode( $context );
}
error_log( $line );
}
Ce n’est pas un système de logging complet, mais ça réduit drastiquement les “echo accidentels”.
Éviter ce problème à l’avenir
- Supprimez la balise PHP finale dans tous les fichiers PHP “purs” (plugins, mu-plugins, includes). C’est une convention très efficace.
- Standardisez l’encodage : UTF‑8 sans BOM, fins de ligne cohérentes. Ajoutez un contrôle dans votre CI si vous avez Git.
- N’affichez jamais les erreurs PHP en production :
- ça casse REST/AJAX,
- ça peut déclencher “headers already sent”,
- ça fuit des chemins serveur et des infos sensibles.
- Remplacez les var_dump/echo par des logs (ou un outil de debug local), et retirez-les avant déploiement.
- Évitez les snippets “collés” dans functions.php si vous pouvez : préférez un petit plugin site-specific ou un mu-plugin. Ça se versionne mieux, et on évite les merges douteux.
- Surveillez les endpoints JSON : un test automatisé qui vérifie qu’un endpoint REST renvoie du JSON valide détecte très tôt les outputs parasites.
Petit garde-fou côté dev : refuser les fichiers avec BOM (Git)
Si vous avez un dépôt Git, une approche simple est de refuser les BOM via un hook de pre-commit (à adapter à votre workflow). Exemple (simplifié) :
# .git/hooks/pre-commit (exemple simplifié)
# Vérifie les fichiers PHP modifiés pour un BOM UTF-8
git diff --cached --name-only -- '*.php' | while read -r file; do
if [ -f "$file" ]; then
if head -c 3 "$file" | grep -q $'xEFxBBxBF'; then
echo "BOM UTF-8 détecté dans $file. Réenregistrez en UTF-8 sans BOM."
exit 1
fi
fi
done
Ressources
- developer.wordpress.org – Debugging in WordPress
- developer.wordpress.org – wp_safe_redirect()
- developer.wordpress.org – wp_redirect()
- developer.wordpress.org – wp_json_encode()
- wordpress.org – Query Monitor
- wordpress.org – Health Check & Troubleshooting
- php.net – header()
- GitHub – WordPress core (wordpress-develop)
- core.trac.wordpress.org – Tickets WordPress
Questions fréquentes
Pourquoi l’erreur mentionne pluggable.php alors que mon code est ailleurs ?
Parce que WordPress échoue à envoyer un header (cookie/redirection) depuis une fonction “pluggable”. La vraie cause est le fichier indiqué dans “output started at …”. C’est lui qui a commencé à écrire.
Est-ce que “headers already sent” peut venir d’un simple warning PHP ?
Oui, si le warning est affiché dans la page (display_errors activé). C’est pour ça que je coupe systématiquement WP_DEBUG_DISPLAY en production et que je logue à la place.
J’ai supprimé l’echo, mais l’erreur reste. Pourquoi ?
Très souvent : cache HTML/CDN, ou vous avez modifié le mauvais fichier (thème parent vs thème enfant, mauvaise release). Purgez les caches et vérifiez que la ligne indiquée correspond bien au fichier réellement servi.
Est-ce que je peux “corriger” en ajoutant ob_start() ?
Vous pouvez masquer le symptôme, pas corriger la cause. En diagnostic temporaire sur staging, oui. En production, évitez : vous risquez d’augmenter la mémoire et de rendre les problèmes plus difficiles à détecter.
Comment repérer un BOM si mon éditeur ne l’affiche pas ?
Le plus fiable : head -c 3 fichier.php | hexdump -C. Si vous voyez ef bb bf, supprimez-le et réenregistrez en UTF‑8 sans BOM.
Pourquoi ça casse surtout Elementor/Divi/Avada ?
Parce que ces builders utilisent beaucoup d’appels AJAX/REST. Un seul caractère avant le JSON rend la réponse invalide, et l’éditeur ne sait plus parser.
Le message “output started at …:1” mais la ligne 1 est “<?php”. Comment c’est possible ?
Typiquement un BOM UTF‑8 (invisible) avant <?php. Ou un caractère non imprimable ajouté lors d’un déploiement.
Est-ce que ça peut venir de wp-config.php même si je n’y touche jamais ?
Oui : certains outils d’hébergement l’éditent (ajout de constantes), et certains éditeurs le réenregistrent avec BOM. C’est un des premiers fichiers à vérifier.
Que faire si je suis bloqué hors admin à cause de l’erreur ?
Passez par SFTP/SSH : renommez temporairement le dossier du plugin suspect dans wp-content/plugins/, ou basculez sur un thème par défaut en renommant le dossier du thème actif. Ensuite, corrigez proprement.