Si vous avez déjà vu “There has been a critical error on this website” côté front, mais rien d’exploitable dans l’admin, vous avez vécu le scénario classique : en production, le debug est soit désactivé (donc aveugle), soit activé n’importe comment (donc risqué).
Le problème
Le besoin réel n’est pas “activer WP_DEBUG”, mais déboguer en production sans :
- afficher des erreurs aux visiteurs,
- exposer des secrets (tokens, chemins serveur, requêtes SQL),
- dégrader les performances,
- remplir le disque avec un
debug.loginterminable.
Les messages typiques que vous allez croiser (dans les logs PHP, dans wp-content/debug.log, ou dans Query Monitor) ressemblent à ça :
PHP Fatal error: Uncaught Error: Call to undefined function myplugin_do_thing()
in /var/www/html/wp-content/plugins/myplugin/includes/hooks.php:42
Stack trace:
#0 /var/www/html/wp-includes/class-wp-hook.php(324): myplugin_boot()
#1 /var/www/html/wp-includes/class-wp-hook.php(348): WP_Hook->apply_filters()
#2 /var/www/html/wp-includes/plugin.php(517): WP_Hook->do_action()
#3 /var/www/html/wp-settings.php(700): do_action('init')
#4 /var/www/html/wp-config.php(92): require_once('...')
#5 /var/www/html/wp-load.php(50): require_once('...')
#6 /var/www/html/wp-blog-header.php(13): require_once('...')
#7 /var/www/html/index.php(17): require('...')
thrown in /var/www/html/wp-content/plugins/myplugin/includes/hooks.php on line 42
Ou, plus insidieux, un bruit de fond :
PHP Warning: Undefined array key "nonce" in /var/www/html/wp-content/themes/child/functions.php on line 118
PHP Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the myplugin domain was triggered too early.
Quand ça apparaît :
- Front-end : pages publiques, templates, shortcodes, widgets, modules Divi/Elementor.
- Admin : écrans d’édition, AJAX, écrans builder.
- Cron : tâches planifiées silencieuses (souvent la source des “ça rame la nuit”).
- API REST : erreurs JSON, 401/403, timeouts, CORS.
Circconstances typiques :
- après une mise à jour WordPress (ici WordPress 6.9.4) ou PHP,
- après activation d’un plugin “innocent” (cache, sécurité, snippets),
- après migration (nouveau serveur, nouveaux chemins, OPcache),
- après activation d’un builder (Divi 5, Elementor, Avada) qui ajoute beaucoup d’AJAX et de requêtes.
À la fin, vous saurez mettre en place une stratégie de debug réversible : logs utiles (corrélés par requête), Query Monitor exploitable sans fuite d’infos, et une méthode pour diagnostiquer REST/AJAX/Cron/WP-CLI.
Résumé rapide
- Ne mettez pas
WP_DEBUG_DISPLAYàtrueen production. Logguez, n’affichez pas. - Utilisez
WP_DEBUG_LOGavec un chemin hors webroot si possible, et ajoutez une rotation. - Ajoutez un Request ID par requête pour corréler PHP logs, Query Monitor, REST et erreurs JS.
- Query Monitor est excellent pour SQL/hooks/templates, mais limité pour Cron/CLI et parfois AJAX.
- Pour REST/AJAX, logguez aussi status code + route + user + nonce (sans secrets).
- Quand c’est intermittent : activez le debug temporairement et uniquement pour votre IP / votre compte.
Les symptômes
Ce que vous observez, du plus courant au plus pénible :
- Erreur 500 aléatoire, mais uniquement sur certaines pages (souvent un template ou un shortcode).
- Écran blanc (fatal error) ou message “critical error”.
- REST API qui renvoie du HTML au lieu de JSON (fatal dans un endpoint), ou
rest_cookie_invalid_nonce. - AJAX admin-ajax.php qui reste en “pending” (souvent un
wp_die()ou une notice qui casse le JSON). - Back-office lent : Query Monitor montre 200+ requêtes SQL, des requêtes dupliquées, ou des appels HTTP bloquants.
- Ça marche en local mais pas en prod : différences PHP (extensions, OPcache), cache agressif, permissions fichiers, règles ModSecurity/WAF.
- Hooks non exécutés : priorité, mauvais hook (ex: code exécuté avant
plugins_loaded), ou conditionnel mal placé. - Modules Divi/Elementor qui ne se rendent pas : erreur PHP silencieuse dans un callback, ou ressources JS non enqueue.
J’ai souvent vu le même pattern sur des sites “optimisés” : un plugin de cache/minification masque l’erreur réelle, et vous vous retrouvez à déboguer un symptôme (JS cassé) alors que la cause est un warning PHP qui pollue une réponse JSON.
Pourquoi ça arrive
Version simple : WordPress exécute du PHP à chaque requête. Si une erreur survient, soit elle est affichée (mauvais en production), soit elle est cachée (vous êtes aveugle), soit elle est logguée (c’est ce qu’on veut).
Version technique : en production, vous avez une chaîne complète :
- PHP (FPM/Apache) + configuration
error_reporting, - WordPress (constants de debug),
- plugin de debug (Query Monitor),
- cache page/objet (Redis/Memcached),
- OPcache, CDN, WAF,
- builders (Divi 5/Elementor/Avada) qui génèrent beaucoup d’AJAX et de REST.
Le même symptôme peut venir de causes différentes. Exemple : un “JSON parse error” côté navigateur peut être :
- un warning PHP injecté avant le JSON,
- un echo oublié dans un callback REST/AJAX,
- un plugin de sécurité qui renvoie une page HTML 403,
- une compression/minification qui modifie la réponse.
Causes probables (du plus fréquent au plus rare) :
- Debug mal configuré :
WP_DEBUGactivé maisWP_DEBUG_LOGnon, ou log inaccessible. - Query Monitor actif pour tout le monde (fuite d’infos + perf).
debug.logexposé publiquement (mauvais droits / webroot).- Volume de logs énorme (notices) qui masque le fatal.
- Problème de Cron/CLI non visible via Query Monitor.
- Erreurs intermittentes liées à la concurrence (race conditions) : transients, options autoload, verrous, cron overlap.
Prérequis avant de commencer
- Sauvegarde fichiers + base (et test de restauration). Ne touchez pas au debug en prod sans plan de rollback.
- WordPress 6.9.4 et PHP 8.1+ (idéalement 8.2/8.3 si votre stack est prête).
- Accès au serveur (au minimum SFTP + logs, idéalement SSH + WP-CLI).
- Plugins utiles :
Docs officielles à garder ouvertes :
- Debugging in WordPress (WP_DEBUG, etc.)
- error_log() (référence PHP côté WP)
- PHP error configuration (php.net)
Solution 1 : Journaliser proprement en production (WP_DEBUG_LOG + rotation + corrélation)
Objectif : obtenir des logs exploitables sans exposition, avec un identifiant de requête pour recoller les morceaux entre PHP, Query Monitor, REST et JS.
1) Configuration “classique” (souvent cassée)
Voici le mauvais classique que je vois encore : affichage activé, log non maîtrisé.
<?php
// wp-config.php (AVANT) — configuration risquée en production
define('WP_DEBUG', true);
define('WP_DEBUG_DISPLAY', true); // Mauvais : fuite d'infos côté visiteurs
define('WP_DEBUG_LOG', true); // Écrit dans wp-content/debug.log (souvent exposé)
@ini_set('display_errors', 1); // Mauvais : force l'affichage même si WP le désactive
Ce que ça provoque :
- les visiteurs voient des chemins serveur, versions, parfois des fragments de requêtes SQL,
- les réponses JSON (REST/AJAX) sont contaminées par des warnings,
- le
debug.loggrossit sans limite.
2) Configuration corrigée (production-safe)
Configuration recommandée pour WordPress 6.9.4+ : on loggue, on n’affiche pas. Et on évite le wp-content/debug.log si on peut.
<?php
// wp-config.php (APRÈS) — debug exploitable sans affichage public
define('WP_DEBUG', true);
// Ne jamais afficher les erreurs en production
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);
// Log WordPress : idéalement un chemin hors webroot
// Exemple : /var/log/wordpress/site-debug.log (à adapter à votre serveur)
define('WP_DEBUG_LOG', '/var/log/wordpress/site-debug.log');
// Optionnel : réduire le bruit (notices) si vous êtes en chasse d'un fatal
// Attention : vous pouvez masquer des signaux faibles, à utiliser temporairement.
@ini_set('error_reporting', E_ALL & ~E_NOTICE & ~E_DEPRECATED);
// Recommandé en prod : désactiver l'éditeur de fichiers
define('DISALLOW_FILE_EDIT', true);
Point serveur : assurez-vous que PHP a le droit d’écrire dans /var/log/wordpress/. Sur FPM, c’est souvent l’utilisateur www-data (Debian/Ubuntu) ou nginx (Alpine/CentOS).
Si vous ne pouvez pas écrire hors webroot, gardez WP_DEBUG_LOG à true mais protégez le fichier via Nginx/Apache. Exemple Nginx :
# nginx.conf / vhost
location ~* /wp-content/debug.log$ {
deny all;
return 403;
}
3) Ajouter un Request ID (corrélation multi-outils)
Sans corrélation, vous perdez un temps fou : une erreur JS dans la console ne se recolle pas facilement à une entrée PHP. Je pose presque toujours un Request ID en MU-plugin.
Créez wp-content/mu-plugins/bpcab-request-id.php :
<?php
/**
* Plugin Name: BPCAB - Request ID
* Description: Ajoute un identifiant de requête pour corréler logs PHP, REST et debug applicatif.
*/
if (!defined('ABSPATH')) {
exit;
}
final class BPCAB_Request_ID {
private static string $request_id = '';
public static function bootstrap(): void {
// Très tôt : MU-plugin chargé avant les plugins classiques
self::$request_id = self::generate_request_id();
// Expose côté PHP
if (!defined('BPCAB_REQUEST_ID')) {
define('BPCAB_REQUEST_ID', self::$request_id);
}
// Ajoute un header HTTP (utile dans DevTools / CDN logs)
add_action('send_headers', [__CLASS__, 'send_header'], 1);
// Ajoute un champ dans les réponses REST
add_filter('rest_post_dispatch', [__CLASS__, 'rest_add_header'], 10, 3);
}
private static function generate_request_id(): string {
// Format court, lisible, suffisamment unique pour corrélation
$time = (string) round(microtime(true) * 1000);
$rand = bin2hex(random_bytes(4));
return $time . '-' . $rand;
}
public static function send_header(): void {
if (!headers_sent()) {
header('X-WP-Request-ID: ' . self::$request_id);
}
}
public static function rest_add_header($response, $server, $request) {
if ($response instanceof WP_REST_Response) {
$response->header('X-WP-Request-ID', self::$request_id);
}
return $response;
}
public static function id(): string {
return self::$request_id;
}
}
BPCAB_Request_ID::bootstrap();
Ensuite, utilisez ce Request ID dans vos logs applicatifs (plugin/thème). Exemple :
<?php
// Exemple dans votre code
error_log('[RID ' . (defined('BPCAB_REQUEST_ID') ? BPCAB_REQUEST_ID : 'n/a') . '] Début traitement checkout');
Ce détail change la donne quand vous déboguez un bug intermittent derrière un CDN ou un cache agressif.
4) Rotation simple du log (éviter le disque plein)
WordPress n’inclut pas de rotation de logs. En production, j’ai déjà vu un debug.log de 8 Go bloquer des mises à jour et casser des uploads.
Si vous avez accès serveur : utilisez logrotate (Linux). Exemple /etc/logrotate.d/wordpress-site :
/var/log/wordpress/site-debug.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
copytruncate est pratique avec PHP-FPM, mais pas parfait : il y a une petite fenêtre de race condition. Si vous pouvez, préférez une rotation avec reopen côté service, mais en mutualisé vous n’aurez pas la main.
Solution 2 : Utiliser Query Monitor sans se tirer une balle dans le pied en production
Query Monitor est excellent pour :
- requêtes SQL (doublons, lenteurs, appels non indexés),
- hooks (priorités, qui appelle quoi),
- templates, conditionnels, HTTP API,
- erreurs PHP capturées pendant la requête.
Mais en production, deux risques reviennent :
- exposition : QM affiche des infos sensibles si visible par le mauvais utilisateur,
- performance : sur un site très chargé, l’overhead n’est pas gratuit.
1) Symptôme : Query Monitor visible par des comptes non-admin
Sur certains sites, des rôles personnalisés (ou des plugins de membership) donnent des capacités proches d’admin. Résultat : QM s’affiche à des utilisateurs qui ne devraient jamais voir SQL/paths.
Ce que je fais : je restreins QM à une capacité stricte (par exemple manage_options) et, si possible, à une liste d’emails ou d’IDs.
Ajoutez un MU-plugin wp-content/mu-plugins/bpcab-qm-guard.php :
<?php
/**
* Plugin Name: BPCAB - Query Monitor Guard
* Description: Restreint Query Monitor à une liste d'utilisateurs / capacité stricte.
*/
if (!defined('ABSPATH')) {
exit;
}
add_filter('qm/dispatchers', function (array $dispatchers): array {
// Si Query Monitor n'est pas actif, ce filtre ne sert à rien, mais il est safe.
if (!is_user_logged_in()) {
return [];
}
$user = wp_get_current_user();
// Capacité minimale : admin
if (!current_user_can('manage_options')) {
return [];
}
// Optionnel : limiter à une liste blanche
$allowed_emails = [
'[email protected]',
];
if (!in_array($user->user_email, $allowed_emails, true)) {
return [];
}
return $dispatchers;
}, 999);
Pourquoi ça marche : Query Monitor utilise des “dispatchers” (barre, HTML, etc.). En renvoyant un tableau vide, vous empêchez l’affichage. Ce pattern est stable depuis longtemps dans QM, mais testez après mise à jour du plugin.
2) Symptôme : back-office qui devient lent avec QM
Quand vous avez Divi 5 / Elementor / Avada, les écrans builder déclenchent beaucoup d’AJAX et de requêtes. QM peut devenir bruyant.
Stratégie : n’activez QM que le temps du diagnostic, et si vous devez le laisser, limitez-le :
- uniquement pour votre compte,
- uniquement sur certains environnements (staging),
- ou uniquement sur certaines URL (ex: une page qui reproduit le bug).
Exemple : désactiver QM automatiquement sauf sur une URL de debug (paramètre ?qm=1) :
<?php
/**
* MU-plugin : n'autoriser QM que si ?qm=1 et utilisateur admin.
* Note : ça n'empêche pas l'activation du plugin, ça coupe l'affichage.
*/
add_filter('qm/dispatchers', function (array $dispatchers): array {
if (!is_user_logged_in() || !current_user_can('manage_options')) {
return [];
}
// Paramètre explicite : évite d'oublier QM allumé partout
$qm = isset($_GET['qm']) ? sanitize_text_field(wp_unslash($_GET['qm'])) : '';
if ($qm !== '1') {
return [];
}
return $dispatchers;
}, 999);
3) Diagnostic avec QM : ce que je regarde en premier
- Queries : tri par “Time”, repérer les requêtes répétées (N+1), et les requêtes sur
wp_optionsavecautoloadproblématique. - Hooks : rechercher un hook surchargé (ex:
initavec 200 callbacks), vérifier les priorités. - HTTP API calls : appels externes lents (API tierce), souvent oubliés dans un admin page load.
- PHP Errors : warnings/notices qui polluent REST/AJAX.
Observation terrain : sur des sites avec cache objet Redis, un plugin peut “marcher” en staging sans Redis, puis exploser en prod à cause d’un objet sérialisé inattendu. QM vous aide à voir les calls et le timing, mais le log PHP corrélé (Solution 1) vous dira quoi a cassé.
Solution 3 : Tracer REST, AJAX, Cron et WP-CLI (les zones où QM ne suffit pas)
Query Monitor brille sur une requête web interactive. Mais vos pires bugs arrivent souvent ailleurs :
- Cron qui tourne sans interface,
- WP-CLI en tâche planifiée,
- REST appelé par un builder,
- AJAX admin-ajax.php avec JSON cassé.
1) REST API : logguer proprement sans fuite
Objectif : quand un endpoint renvoie 403/500, vous voulez la route, l’utilisateur, et le Request ID. Pas le contenu du token.
<?php
// MU-plugin : log minimal REST (route + status + user + RID)
add_filter('rest_post_dispatch', function ($response, $server, $request) {
$route = method_exists($request, 'get_route') ? $request->get_route() : 'n/a';
$method = method_exists($request, 'get_method') ? $request->get_method() : 'n/a';
$status = 200;
if ($response instanceof WP_REST_Response) {
$status = (int) $response->get_status();
} elseif (is_wp_error($response)) {
$status = 500;
}
// Ne logguez pas le body, ni les headers Authorization
$user_id = get_current_user_id();
$rid = defined('BPCAB_REQUEST_ID') ? BPCAB_REQUEST_ID : 'n/a';
// Ne logguez que les erreurs et les 4xx/5xx pour réduire le bruit
if ($status >= 400) {
error_log(sprintf(
'[RID %s] REST %s %s status=%d user=%d',
$rid,
$method,
$route,
$status,
$user_id
));
}
return $response;
}, 10, 3);
Edge case : certains plugins renvoient des WP_Error à la place d’une WP_REST_Response. D’où le test is_wp_error().
2) AJAX admin-ajax.php : capturer les sorties parasites
Le bug que je vois le plus : un echo ou une notice PHP casse la réponse JSON. Exemple “avant” :
<?php
// AVANT : callback AJAX cassé (echo + sortie non JSON)
add_action('wp_ajax_my_action', function () {
echo "debug"; // Mauvais : pollue la réponse
$data = [
'ok' => true,
];
wp_send_json_success($data);
});
“Après” : aucune sortie parasite, check nonce, et log en cas d’erreur.
<?php
// APRÈS : callback robuste + logs corrélés
add_action('wp_ajax_my_action', function () {
$rid = defined('BPCAB_REQUEST_ID') ? BPCAB_REQUEST_ID : 'n/a';
// Vérification nonce (sécurité)
check_ajax_referer('my_action_nonce', 'nonce');
try {
$data = [
'ok' => true,
];
wp_send_json_success($data);
} catch (Throwable $e) {
// Ne pas exposer la stack au client
error_log(sprintf('[RID %s] AJAX my_action fatal: %s', $rid, $e->getMessage()));
wp_send_json_error(['message' => 'Erreur serveur'], 500);
}
});
Note : depuis PHP 7+, Throwable capture Error + Exception. En prod, c’est souvent ce que vous voulez pour logguer proprement un fatal dans un handler.
3) Cron : journaliser et éviter les chevauchements
Cron WordPress est sujet aux overlaps. Sur un site à trafic, deux requêtes peuvent déclencher WP-Cron quasi en même temps. Résultat : double traitement, deadlocks, ou charge qui explose.
Pattern que j’utilise : un verrou via transient (simple) ou via cache objet (mieux si Redis). Exemple :
<?php
// AVANT : tâche cron sans verrou (peut se chevaucher)
add_action('bpcab_hourly_job', function () {
// Traitement long...
do_action('bpcab_sync_remote');
});
// APRÈS : verrou + logs
add_action('bpcab_hourly_job', function () {
$rid = defined('BPCAB_REQUEST_ID') ? BPCAB_REQUEST_ID : 'n/a';
$lock_key = 'bpcab_hourly_job_lock';
$lock_ttl = 15 * MINUTE_IN_SECONDS;
if (get_transient($lock_key)) {
error_log(sprintf('[RID %s] CRON bpcab_hourly_job ignoré (verrou actif)', $rid));
return;
}
set_transient($lock_key, 1, $lock_ttl);
try {
error_log(sprintf('[RID %s] CRON bpcab_hourly_job démarrage', $rid));
do_action('bpcab_sync_remote');
error_log(sprintf('[RID %s] CRON bpcab_hourly_job fin', $rid));
} catch (Throwable $e) {
error_log(sprintf('[RID %s] CRON bpcab_hourly_job erreur: %s', $rid, $e->getMessage()));
} finally {
delete_transient($lock_key);
}
});
Edge case : si PHP meurt avant le finally, le verrou reste jusqu’au TTL. C’est voulu : mieux vaut rater un run que doubler un traitement destructeur.
4) WP-CLI : centraliser les logs
WP-CLI n’utilise pas Query Monitor. Si vous lancez des commandes via cron système, logguez explicitement.
# Exemple cron système
*/15 * * * * cd /var/www/html && wp myplugin sync --quiet >> /var/log/wordpress/wp-cli.log 2>&1
Et dans votre commande WP-CLI (si vous la maintenez), utilisez aussi le Request ID (ou un job ID) pour recoller avec les logs applicatifs.
Vérifications après correction
- Le front ne montre aucun warning (inspectez le HTML rendu, et testez une route REST).
- Le header
X-WP-Request-IDest présent (DevTools > Network). - Le fichier de log reçoit des entrées et n’est pas accessible publiquement (testez une URL directe si dans webroot).
- Query Monitor n’est visible que pour vous (testez en navigation privée ou avec un compte éditeur).
- AJAX/REST renvoient du JSON valide (pas de HTML injecté).
- Les tâches Cron ne se chevauchent plus (vous voyez “verrou actif” au besoin, mais pas de double exécution).
Tableau de diagnostic rapide
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| JSON invalide (REST/AJAX) | Warning/echo dans la réponse | DevTools > Network > Response contient du HTML/texte | Couper l’affichage, supprimer echo, logguer côté serveur |
| Erreur 500 aléatoire | Fatal intermittent, race condition cron | Corréler par Request ID, regarder les timestamps | Verrou transient + logs corrélés |
| QM visible à trop de monde | Rôles/capacités trop larges | Tester avec un compte non-admin | Filtre qm/dispatchers (liste blanche) |
| Site lent en admin | Appels HTTP bloquants / N+1 queries | QM > HTTP API / Queries triées par temps | Cache, réduction requêtes, déplacer traitements hors requête |
| Disque plein | Log non rotaté | Taille du fichier log, erreurs “No space left on device” | logrotate / réduction bruit / logs ciblés |
Si ça ne marche toujours pas
- Videz les caches (plugin cache, cache serveur, CDN). J’ai déjà vu un vieux JS minifié continuer à appeler un endpoint supprimé.
- Activez Health Check et utilisez le mode “Troubleshooting” pour isoler un conflit plugin/thème sans impacter les visiteurs : Health Check & Troubleshooting.
- Vérifiez les logs PHP-FPM/Apache en parallèle de
WP_DEBUG_LOG. Certains fatals (segfault, OOM) n’apparaissent pas côté WordPress. - Augmentez la mémoire si vous voyez des OOM :
Allowed memory size of ... bytes exhausteddans les logs- ou kill silencieux par l’OS (OOM killer)
- Testez les permaliens : régénérez (Réglages > Permaliens > Enregistrer). Une règle de réécriture cassée peut ressembler à un bug REST.
- Console navigateur : cherchez des 401/403/500, des erreurs CORS, et comparez avec le Request ID côté headers.
- Vérifiez PHP : une extension manquante (intl, mbstring) ou une différence de config entre local et prod explique beaucoup de “ça marche chez moi”.
- OPcache : si vous déployez sans reset OPcache, vous pouvez exécuter un mélange de fichiers anciens/nouveaux. Symptôme : erreurs sur des lignes qui ne correspondent pas au fichier actuel.
Si vous êtes derrière un WAF (Cloudflare, ModSecurity), testez temporairement sans règles agressives. Les faux positifs sur admin-ajax.php et /wp-json/ sont fréquents.
Pièges et erreurs courantes
| Symptôme | Cause probable | Solution recommandée |
|---|---|---|
| Le site affiche des warnings aux visiteurs | WP_DEBUG_DISPLAY ou display_errors activé |
WP_DEBUG_DISPLAY=false + @ini_set('display_errors', 0) |
debug.log vide |
Chemin non inscriptible / permissions | Corriger droits, utiliser un chemin valide, vérifier l’utilisateur PHP-FPM |
debug.log accessible via URL |
Fichier dans webroot non protégé | Déplacer hors webroot ou bloquer via Nginx/Apache |
| Erreur après copie d’un snippet | Snippet collé dans le mauvais fichier / oubli d’un ; |
Mettre en MU-plugin, valider syntaxe, déployer via Git si possible |
| Hook “ne marche pas” | Mauvais hook ou priorité | Vérifier avec QM > Hooks, déplacer vers plugins_loaded / init selon le cas |
REST 403 rest_cookie_invalid_nonce |
Nonce manquant/expiré, cache HTML d’une page qui contient un nonce | Ne pas cacher les pages avec nonce, régénérer nonce, vérifier check_ajax_referer |
| CSS/JS non chargé | Mauvais enqueue (hook, dépendances), minification | Vérifier wp_enqueue_scripts/admin_enqueue_scripts, désactiver minification pour tester |
| Code d’un ancien tuto cassé en WP 6.9.4 | API changée, fonctions appelées trop tôt | Revoir le hook, vérifier la doc officielle, éviter les snippets non datés |
| Test direct en production | Pas de staging/rollback | Utiliser un environnement de test et activer le debug de façon temporaire |
Erreur que je vois souvent chez des utilisateurs avancés : activer QM, voir “plein de requêtes”, puis optimiser au hasard. Commencez par isoler la requête la plus lente et identifiez qui la déclenche (QM vous donne le stack/caller). Sans ça, vous risquez d’optimiser un écran qui n’est pas le problème.
Variante / alternative
Méthode sans code (plugin + hébergeur)
- Utilisez Query Monitor uniquement sur staging.
- Activez les logs PHP via votre hébergeur (cPanel/Plesk) et récupérez-les via l’interface.
- Utilisez Health Check en mode Troubleshooting pour isoler un conflit sans impacter le trafic.
Limite : vous perdez la corrélation fine (Request ID) et vous dépendez du format de logs de l’hébergeur.
Méthode plus avancée (observabilité)
Si vous avez la main sur l’infra, passez à une vraie collecte :
- centralisation (ELK/Opensearch),
- traces (OpenTelemetry),
- profiling (tideways/blackfire selon votre stack).
WordPress n’est pas “contre” ces outils, mais vous devez être discipliné sur ce que vous logguez (RGPD, secrets). Dans mon expérience, c’est le seul moyen de diagnostiquer rapidement un bug rare sur un site à fort trafic.
Éviter ce problème à l’avenir
- Mettre le debug dans des MU-plugins : chargés tôt, versionnés, et moins sujets aux désactivations accidentelles.
- Ne logguez jamais :
- mots de passe,
- headers
Authorization, - cookies,
- contenu complet des formulaires (PII).
- Réduisez le bruit : corrigez les notices au lieu de les ignorer. Une prod propre en warnings vous permet de repérer un vrai signal en 10 secondes.
- Évitez les echos dans REST/AJAX. Utilisez
wp_send_json_*()et retournez toujours une structure cohérente. - Builders :
- Divi 5 / Elementor / Avada déclenchent beaucoup d’actions AJAX. Testez vos endpoints avec cache désactivé, puis réactivez le cache progressivement.
- Si un module custom fait des appels REST, ajoutez systématiquement le Request ID dans les headers pour corrélation.
- Déploiement : si vous déployez par FTP, vous augmentez les risques de fichiers partiellement uploadés. Préférez un déploiement atomique (release dir + symlink) quand c’est possible.
Je garde aussi une règle simple : tout ce qui est “temporaire” doit s’éteindre tout seul. D’où le filtre QM conditionné à ?qm=1 : vous évitez d’oublier un outil de debug actif pendant des semaines.
Ressources
- Documentation officielle : Debugging in WordPress
- Plugin Query Monitor (wordpress.org)
- Documentation REST API (developer.wordpress.org)
- wp_send_json_success() (référence)
- check_ajax_referer() (référence)
- PHP : error_log() (php.net)
- WordPress Core Trac (tickets et historique)
- Miroir GitHub de wordpress-develop (références PR/commits)
Questions fréquentes
Est-ce que je peux laisser WP_DEBUG activé en production ?
Oui, si vous n’affichez rien (WP_DEBUG_DISPLAY=false) et si vous contrôlez le log (chemin, permissions, rotation). Le risque principal n’est pas WP_DEBUG en soi, c’est l’exposition et le bruit.
WP_DEBUG_LOG écrit où exactement ?
Si WP_DEBUG_LOG vaut true, WordPress écrit dans wp-content/debug.log. Si vous mettez une chaîne (chemin), WordPress tente d’écrire à cet endroit. Vérifiez les permissions.
Query Monitor peut-il casser les performances ?
Sur un site à faible trafic, l’impact est souvent négligeable. Sur un admin builder (Divi 5/Elementor/Avada) ou sur une page avec beaucoup de requêtes, l’overhead est réel. Restreignez-le à votre compte et à un mode “à la demande”.
Pourquoi mon endpoint REST renvoie du HTML au lieu du JSON ?
Le plus fréquent : un warning PHP ou un echo injecté avant la réponse JSON, ou un plugin de sécurité qui renvoie une page 403 HTML. Regardez la réponse brute dans DevTools et corrélez avec le Request ID.
Je ne vois rien dans debug.log, mais j’ai une erreur 500
C’est souvent un fatal en amont (segfault, OOM, timeout) loggué par PHP-FPM/Apache, pas par WordPress. Vérifiez les logs serveur, et augmentez temporairement le niveau de logs PHP côté FPM si vous avez la main.
Comment déboguer un problème Cron qui n’apparaît jamais quand je teste ?
Ajoutez des logs dans le callback Cron, plus un verrou (transient) pour éviter les overlaps. Si possible, déclenchez la tâche via WP-CLI pour la reproduire sans trafic.
Est-ce dangereux d’ajouter un header X-WP-Request-ID ?
Non, tant que l’ID ne contient aucune info sensible (pas d’IP, pas d’email, pas de session). Utilisez un identifiant aléatoire/court comme dans l’exemple.
Dois-je désactiver WP_DEBUG une fois le bug corrigé ?
Si votre politique de prod est stricte, oui. Sinon, vous pouvez le laisser activé avec log maîtrisé, mais assurez-vous que la rotation est en place et que vous ne logguez pas de données sensibles.
Comment éviter que des nonces cassent le cache ?
Ne mettez pas en cache page des pages qui contiennent des nonces destinés à des actions sensibles, ou servez les nonces via un endpoint non caché. Les nonces expirent et un cache HTML peut figer une valeur invalide.
Quel est le meilleur endroit pour mettre ces snippets ?
En production, privilégiez un MU-plugin versionné. Les plugins de snippets sont pratiques, mais j’ai déjà vu un snippet cassé bloquer l’admin, et vous perdez alors l’interface qui permet de le désactiver.