Si vous avez déjà vu un “Fatal error on save_post” au moment de cliquer sur Mettre à jour ou Publier, vous savez à quel point ça coupe net : écran blanc, erreur 500, contenu non enregistré, et parfois un éditeur Gutenberg qui se met à tourner dans le vide.
Le problème
L’erreur est presque toujours déclenchée par du code exécuté sur l’action WordPress save_post. Une action (hook) est un point d’accroche où WordPress exécute votre fonction. save_post se déclenche quand un contenu (article, page, CPT) est enregistré, y compris lors d’autosauvegardes, de révisions et parfois via l’API REST.
Vous verrez typiquement un message de ce genre dans vos logs PHP (ou affiché si le debug est activé) :
Fatal error: Uncaught Error: Call to a member function ... on null in /wp-content/plugins/mon-plugin/mon-plugin.php:123
Stack trace:
#0 /wp-includes/class-wp-hook.php(324): ma_fonction_save_post()
#1 /wp-includes/class-wp-hook.php(348): WP_Hook->apply_filters()
#2 /wp-includes/plugin.php(517): WP_Hook->do_action()
#3 /wp-includes/post.php(5068): do_action('save_post', 42, Object(WP_Post), true)
...
Où ça apparaît :
- Admin : au clic sur “Mettre à jour / Publier”, ou lors d’une mise à jour rapide.
- REST API : en enregistrant depuis l’éditeur de blocs (Gutenberg) ou une intégration externe.
- Cron : plus rare, mais possible si un plugin met à jour des posts en tâche planifiée.
Circonstances typiques que j’ai souvent croisées :
- Après avoir collé un snippet “trouvé sur Internet” dans
functions.php. - Après la mise à jour d’un plugin de champs (ACF-like) ou d’un builder (Divi/Elementor/Avada) qui change le timing des métadonnées.
- Après un passage à PHP 8.1+ où des erreurs qui “passaient” avant deviennent fatales (types, null, méthodes supprimées).
À qui ça s’adresse : si vous êtes blogueur débutant mais que vous avez accès à l’admin WordPress et idéalement au FTP/gestionnaire de fichiers, vous pourrez diagnostiquer la cause, corriger le hook save_post, et vérifier que l’enregistrement fonctionne à nouveau sous WordPress 6.9.4 et PHP 8.1+.
Résumé rapide
- 90% du temps, le hook
save_posts’exécute pendant une autosauvegarde/révision et votre code n’était pas prévu pour ça. - Vérifiez d’abord les garde-fous : nonce, capabilities, type de post, autosave, révision.
- Deuxième cause fréquente : récursion (vous appelez
wp_update_post()danssave_post→ boucle infinie → fatal). - Activez des logs propres (
WP_DEBUG_LOG) et utilisez Query Monitor + Health Check pour isoler le plugin/thème fautif. - Sur Divi 5 / Elementor / Avada : le problème vient souvent d’un snippet “global” qui touche tous les post types, y compris les templates du builder.
Les symptômes
Selon le contexte, vous pouvez observer :
- Erreur 500 au moment d’enregistrer, puis retour au tableau de bord.
- Écran blanc (WSOD) après “Mettre à jour”.
- L’éditeur de blocs affiche “Mise à jour échouée” / “La réponse n’est pas une réponse JSON valide” (car le fatal casse la réponse REST).
- Le contenu est enregistré partiellement : titre ok, mais métadonnées non mises à jour.
- En “Mode classique” ou Quick Edit, l’enregistrement échoue aussi.
- Le problème n’existe qu’avec un rôle non-admin (droits/capacités).
- Ça marche en local, mais pas en production (différences PHP, mémoire, cache objet, extensions).
Signaux qui orientent :
- Si vous voyez “Invalid JSON response” dans Gutenberg, ouvrez la console navigateur : souvent vous verrez une requête
/wp-json/wp/v2/...en erreur 500. - Si le fatal n’apparaît pas à l’écran, il est presque toujours dans
wp-content/debug.logou dans les logs PHP du serveur. - Si le site utilise un cache agressif (plugin de cache + CDN), vous pouvez avoir l’impression que “ça ne marche pas” alors que la correction est faite. Videz le cache admin et navigateur.
Tableau de diagnostic (symptôme → cause → vérification → solution)
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Erreur 500 à l’enregistrement | Fatal PHP dans save_post |
Consultez debug.log / logs serveur, cherchez “save_post” |
Appliquez Solution 1 ou 2 selon le message |
| “Réponse JSON invalide” (Gutenberg) | Fatal pendant la réponse REST | Onglet Réseau (DevTools), requête REST en 500 | Corriger le hook + vérifier nonces/capacités |
| Ça ne casse que sur certaines pages | Conflit avec CPT/templates builder | Désactiver plugins via Health Check (mode dépannage) | Limiter par post type + conditions |
| Boucle / timeout / mémoire épuisée | Récursion wp_update_post() dans save_post |
Log “Allowed memory size exhausted” ou “maximum execution time” | Solution 2 (remove_action / garde) |
| Erreur “nonce” / droits | Vérif de sécurité manquante ou incorrecte | Test avec éditeur vs admin, inspecter $_POST |
Ajouter nonce + current_user_can() |
Pourquoi ça arrive
Explication simple (débutant)
save_post se déclenche souvent, pas seulement quand vous cliquez sur “Publier”. WordPress sauvegarde des brouillons automatiquement, crée des révisions, et peut enregistrer via l’API REST. Si votre code suppose que certains champs existent toujours, ou que l’utilisateur a toujours les droits, ou que vous êtes toujours sur “article”, il finira par tomber sur un cas où ce n’est pas vrai… et PHP 8.1+ ne pardonne pas les erreurs de type ou les appels sur null.
Explication technique (intermédiaire/pro)
L’action save_post reçoit trois paramètres : l’ID du post, l’objet WP_Post, et un booléen $update. Elle est exécutée pendant wp_insert_post(). Les erreurs fatales viennent typiquement de :
- Autosave/révision : vous mettez à jour des metas sur une révision, ou vous lisez
$_POSTalors que le contexte n’est pas celui attendu. - Récursion : vous appelez
wp_update_post()(ouwp_insert_post()) dans votre callbacksave_post, ce qui déclenche…save_postà nouveau. - Mauvais hook : vous utilisez
save_postalors que vous vouliezsave_post_{post_type}(plus précis), ou un hook de meta box. - Objets non chargés : vous appelez une fonction de plugin (ACF-like, builder) alors que ses fonctions ne sont pas disponibles à ce moment (ordre de chargement / conditions admin).
- Incompatibilité PHP : anciennes libs, appels à des méthodes supprimées, erreurs de typage, paramètres manquants.
Causes classées du plus fréquent au plus rare (sur des sites WordPress 6.9.4 que je dépanne) :
- Absence de garde-fous (autosave/révision/capabilities/nonce).
- Récursion via
wp_update_post()ou mise à jour de meta qui déclenche d’autres hooks. - Snippet collé au mauvais endroit (thème parent, plugin de snippets qui s’exécute partout, ou fichier tronqué).
- Conflit plugin (SEO, cache, builder, champs personnalisés) qui modifie le payload de sauvegarde.
- Erreur serveur (mémoire PHP trop basse, OPcache instable, extension manquante) révélée au moment du save.
Prérequis avant de commencer
- Sauvegarde : faites une sauvegarde fichiers + base de données. Ne testez pas “à l’aveugle” en production.
- Environnement de test : si possible, reproduisez sur un staging.
- Versions : WordPress 6.9.4, PHP 8.1+ (recommandé). Si vous êtes en dessous, corrigez d’abord la version PHP.
- Outils :
- Query Monitor (profilage, hooks, erreurs PHP, requêtes).
- Health Check & Troubleshooting (mode dépannage sans impacter les visiteurs).
- Accès au fichier
wp-config.php(ou équivalent via votre hébergeur).
Activez un logging propre (sans afficher les erreurs aux visiteurs). Dans wp-config.php, juste avant “That’s all…” :
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true ); // Écrit dans wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // Évite d'afficher les erreurs à l'écran
@ini_set( 'display_errors', '0' ); // Force côté PHP si possible
Où lire les logs :
wp-content/debug.logsiWP_DEBUG_LOGest actif.- Logs PHP de l’hébergeur (souvent dans le panneau : “Erreurs”, “Logs”).
Solution 1 : Sécuriser votre hook save_post (autosave, révisions, capacités)
Le problème le plus courant : une fonction attachée à save_post qui manipule des metas ou des champs sans vérifier le contexte. Sur WordPress 6.9.4, ça casse particulièrement avec Gutenberg (REST), les builders et les autosaves.
Où appliquer le code :
- Idéal : un plugin custom (recommandé) ou un mu-plugin (
wp-content/mu-plugins/) pour éviter qu’un changement de thème désactive la correction. - À défaut :
functions.phpdu thème enfant (jamais le thème parent).
Sauvegardez avant de modifier. Une parenthèse oubliée dans functions.php peut vous bloquer l’accès à l’admin.
Code AVANT (cassé)
Exemple réaliste : un snippet qui enregistre un champ “Sous-titre” depuis $_POST, sans garde-fous.
add_action( 'save_post', 'mon_sous_titre_save' );
function mon_sous_titre_save( $post_id ) {
// ERREURS FRÉQUENTES :
// - $_POST['mon_sous_titre'] n'existe pas lors d'un autosave ou via REST
// - Pas de vérification de droits
// - Pas de vérification de nonce
// - S'exécute sur TOUS les post types (y compris révisions/templates builder)
update_post_meta( $post_id, '_mon_sous_titre', sanitize_text_field( $_POST['mon_sous_titre'] ) );
}
Ce code peut provoquer :
Undefined array key "mon_sous_titre"(avertissement) qui devient fatal si une autre erreur se produit, ou si une logique dépend de cette valeur.- Des mises à jour de meta sur des révisions (comportement incohérent).
- Des erreurs de droits si un rôle éditeur n’a pas la capacité attendue.
Code APRÈS (corrigé, robuste)
Version solide, compatible WordPress 6.9.4 / PHP 8.1+. Elle gère autosave, révisions, nonce, capacités, post type, et supporte REST (où $_POST peut être vide).
/**
* Enregistre un sous-titre en meta de manière sécurisée.
*
* À placer dans un plugin custom, un mu-plugin, ou functions.php du thème enfant.
*/
add_action( 'save_post', 'mon_sous_titre_save', 10, 3 );
function mon_sous_titre_save( $post_id, $post, $update ) {
// 1) Éviter autosaves et révisions.
if ( wp_is_post_autosave( $post_id ) ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
// 2) Limiter au bon type de contenu (ex: 'post').
// Adaptez selon votre besoin : 'page', 'product', etc.
if ( $post instanceof WP_Post && $post->post_type !== 'post' ) {
return;
}
// 3) Vérifier les droits (capability).
// Pour un 'post', la capacité la plus sûre est edit_post sur l'ID.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// 4) Vérifier un nonce si votre champ vient d'un formulaire/metabox.
// Si vous n'avez pas encore de nonce, voyez plus bas.
if ( isset( $_POST['mon_sous_titre_nonce'] ) ) {
$nonce = sanitize_text_field( wp_unslash( $_POST['mon_sous_titre_nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'mon_sous_titre_action' ) ) {
return;
}
} else {
// Si votre sauvegarde vient de REST (Gutenberg) ou d'un autre flux,
// vous pouvez choisir :
// - soit de ne rien faire si le nonce n'existe pas,
// - soit d'accepter une autre source (ex: meta enregistrée via register_meta).
// Ici on choisit la prudence : pas de nonce => pas de write.
return;
}
// 5) Récupérer la valeur de manière sûre.
// wp_unslash() est nécessaire car WordPress ajoute des slashes dans $_POST.
$sous_titre = '';
if ( isset( $_POST['mon_sous_titre'] ) ) {
$sous_titre = sanitize_text_field( wp_unslash( $_POST['mon_sous_titre'] ) );
}
// 6) Mettre à jour / supprimer proprement.
if ( $sous_titre === '' ) {
delete_post_meta( $post_id, '_mon_sous_titre' );
return;
}
update_post_meta( $post_id, '_mon_sous_titre', $sous_titre );
}
Pourquoi ça corrige
- Autosave / révision : ces enregistrements n’ont pas votre formulaire complet. Sans exclusion, votre code lit des champs inexistants.
- Nonce : c’est un jeton anti-CSRF. Sans nonce, n’importe quelle page pourrait tenter de déclencher une sauvegarde en arrière-plan (risque sécurité).
- Capacités : évite qu’un utilisateur sans droits déclenche une erreur (et évite aussi des écritures non autorisées).
- Post type : Divi 5, Elementor et Avada créent des contenus techniques (templates, layouts). Un snippet “global” qui s’exécute partout finit souvent par casser sur ces post types.
Ajouter le nonce (si vous avez une metabox)
Si votre champ est rendu via une metabox classique, ajoutez un nonce dans votre HTML. Exemple minimal :
function mon_sous_titre_metabox_html( $post ) {
// Nonce : à vérifier dans save_post
wp_nonce_field( 'mon_sous_titre_action', 'mon_sous_titre_nonce' );
$value = get_post_meta( $post->ID, '_mon_sous_titre', true );
echo '<label for="mon_sous_titre">Sous-titre</label>';
echo '<input type="text" id="mon_sous_titre" name="mon_sous_titre" value="' . esc_attr( $value ) . '" />';
}
Si vous utilisez Gutenberg et des metas exposées, une approche plus moderne est d’enregistrer la meta via register_post_meta() avec show_in_rest. Ça évite de dépendre de $_POST et réduit les surprises sur save_post (j’en reparle dans “Éviter ce problème à l’avenir”).
Solution 2 : Éviter la récursion (wp_update_post dans save_post)
Deuxième grand classique : vous modifiez le post (titre, slug, contenu) dans le callback save_post avec wp_update_post(). Problème : wp_update_post() déclenche à nouveau save_post. Si vous ne cassez pas la boucle, vous partez en récursion jusqu’au crash (mémoire, temps d’exécution, fatal).
Symptômes typiques
- Le site “freeze” au moment de publier, puis erreur 500.
- Dans les logs :
Allowed memory size exhaustedouMaximum execution time. - Ou un stack trace énorme où
save_postapparaît en boucle.
Code AVANT (cassé)
Exemple : auto-ajouter un préfixe au titre.
add_action( 'save_post', 'mon_prefixe_titre' );
function mon_prefixe_titre( $post_id ) {
$titre = get_the_title( $post_id );
// Ajoute un préfixe si absent
if ( strpos( $titre, '[Promo] ' ) !== 0 ) {
wp_update_post( array(
'ID' => $post_id,
'post_title' => '[Promo] ' . $titre,
) );
}
}
Ce code a l’air logique. En pratique, il peut boucler (selon révisions, autosaves, filtres de titre, ou si un autre plugin retouche le titre aussi).
Code APRÈS (corrigé)
Deux stratégies fiables :
- Stratégie A : retirer temporairement l’action avant d’appeler
wp_update_post(), puis la remettre. - Stratégie B : utiliser un “verrou” (flag statique) pour empêcher la ré-entrée.
Je privilégie souvent la stratégie A car elle évite des effets de bord si votre callback est appelé plusieurs fois dans le même cycle.
add_action( 'save_post', 'mon_prefixe_titre_corrige', 10, 3 );
function mon_prefixe_titre_corrige( $post_id, $post, $update ) {
// Garde-fous minimum
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( ! ( $post instanceof WP_Post ) ) {
return;
}
if ( $post->post_type !== 'post' ) {
return;
}
$titre = (string) $post->post_title;
if ( str_starts_with( $titre, '[Promo] ' ) ) {
return;
}
// Retirer l'action pour éviter la récursion
remove_action( 'save_post', 'mon_prefixe_titre_corrige', 10 );
wp_update_post(
array(
'ID' => $post_id,
'post_title' => '[Promo] ' . $titre,
),
true // Renvoie WP_Error en cas d'échec
);
// Remettre l'action
add_action( 'save_post', 'mon_prefixe_titre_corrige', 10, 3 );
}
Pourquoi ça corrige
remove_action()empêche votre propre fonction d’être rappelée par l’update que vous déclenchez vous-même.- Les garde-fous évitent de modifier des révisions et de déclencher des écritures inattendues via REST/autosave.
- En passant
trueen second paramètre dewp_update_post(), WordPress renvoie unWP_Errorau lieu de “cacher” certains problèmes. En dépannage, c’est précieux.
Note pour Divi 5 / Elementor / Avada
J’ai souvent vu cette récursion sur des sites où un builder enregistre des “templates” comme des posts. Si vous préfixez tous les titres sans filtrer le post_type, vous finissez par modifier un layout du builder, qui déclenche un autre flux de sauvegarde interne. Résultat : boucle. Filtrez strictement par post type et, si besoin, par écran admin.
Solution 3 : Isoler la source avec logs, Query Monitor, Health Check et WP-CLI
Quand vous n’avez pas écrit le code fautif (plugin premium, snippet ancien, prestataire), le plus efficace est d’isoler qui accroche save_post et casse l’enregistrement.
Étape 1 : récupérer le message exact
Sans le message exact, on tourne vite en rond. Cherchez dans :
wp-content/debug.log- Logs serveur (Apache/Nginx + PHP-FPM)
Vous cherchez surtout :
- Le fichier et la ligne (
/wp-content/plugins/...:123) - Le type d’erreur (
Call to undefined function,Call to a member function ... on null,TypeError)
Étape 2 : Query Monitor
Installez Query Monitor, puis reproduisez l’erreur. Sur certaines erreurs fatales, Query Monitor n’aura pas le temps d’afficher. Mais quand ça passe, il vous donne :
- Les erreurs PHP et leurs traces
- Les hooks exécutés
- Les requêtes REST en admin
Plugin officiel : https://wordpress.org/plugins/query-monitor/
Étape 3 : Health Check (mode dépannage)
Le mode dépannage est une arme sous-estimée : vous pouvez désactiver tous les plugins pour vous uniquement (les visiteurs ne voient rien). Ensuite vous réactivez un par un jusqu’à reproduire le fatal.
- Installez Health Check & Troubleshooting
- Activez “Mode dépannage”
- Testez un enregistrement
- Réactivez plugins/thème un par un
Dans mon expérience, c’est le moyen le plus rapide de prouver “c’est tel plugin” sans casser le site en production.
Étape 4 : lister les callbacks sur save_post (snippet de diagnostic)
Si vous avez accès au code, vous pouvez loguer ce qui est attaché à save_post. Ce n’est pas un outil grand public, mais en dépannage c’est redoutable.
Où coller : mu-plugin temporaire (recommandé). Créez wp-content/mu-plugins/diag-save-post.php.
<?php
/**
* Plugin Name: DIAG - save_post callbacks
* Description: Diagnostic temporaire : logue les callbacks attachés à save_post.
* Author: Vous
*/
add_action( 'admin_init', function () {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wp_filter;
if ( empty( $wp_filter['save_post'] ) ) {
error_log( '[DIAG save_post] Aucun callback sur save_post.' );
return;
}
$hook = $wp_filter['save_post'];
// WP_Hook stocke les callbacks par priorité.
$callbacks = $hook->callbacks ?? array();
error_log( '[DIAG save_post] --- Début liste callbacks ---' );
foreach ( $callbacks as $priority => $items ) {
foreach ( $items as $item ) {
$fn = $item['function'];
if ( is_string( $fn ) ) {
error_log( "[DIAG save_post] priority={$priority} function={$fn}" );
} elseif ( is_array( $fn ) ) {
// [objet, méthode] ou [classe, méthode]
$who = is_object( $fn[0] ) ? get_class( $fn[0] ) : (string) $fn[0];
$method = (string) $fn[1];
error_log( "[DIAG save_post] priority={$priority} method={$who}::{$method}" );
} elseif ( $fn instanceof Closure ) {
error_log( "[DIAG save_post] priority={$priority} closure" );
} else {
error_log( "[DIAG save_post] priority={$priority} callback_inconnu" );
}
}
}
error_log( '[DIAG save_post] --- Fin liste callbacks ---' );
} );
Ensuite, rechargez l’admin et regardez debug.log. Vous verrez quels plugins/thèmes accrochent save_post. Vous pourrez ensuite inspecter le plus suspect (priorité élevée, plugin récent, etc.).
Important : supprimez ce mu-plugin après diagnostic. Il logue beaucoup.
Étape 5 : WP-CLI (avancé, mais très pratique)
Si vous avez WP-CLI, vous pouvez rapidement vérifier versions et santé :
wp --info
wp core version
wp plugin list --status=active
wp theme list
wp config get WP_DEBUG
wp doctor check --all
WP-CLI : https://wp-cli.org/
Vérifications après correction
Après avoir appliqué une correction, testez dans cet ordre :
- Enregistrement simple : éditez un article, modifiez une phrase, cliquez “Mettre à jour”. Attendez un message de succès.
- Gutenberg : vérifiez qu’il n’y a plus “Réponse JSON invalide”. Ouvrez l’onglet Réseau : la requête REST doit renvoyer 200.
- Rôles : testez avec un compte Éditeur (ou Auteur) si votre site en a. Beaucoup de fatals viennent d’un
current_user_can()absent. - Autosave : laissez l’éditeur ouvert 1 à 2 minutes. Si votre code cassait sur autosave, vous verrez des erreurs revenir.
- Builders (si concernés) : éditez une page Elementor / Divi / Avada et sauvegardez. Si votre snippet touchait tous les post types, c’est ici que ça réapparaît.
Puis :
- Videz les caches (plugin de cache, serveur, CDN) si vous avez modifié un mu-plugin ou un plugin custom.
- Surveillez
debug.logpendant 10 minutes d’édition : zéro nouvelle entrée est l’objectif.
Si ça ne marche toujours pas
Quand l’erreur persiste, je procède comme suit.
1) Revenir à un état “minimal” sans casser les visiteurs
- Activez Health Check → mode dépannage.
- Repassez temporairement sur un thème par défaut (si possible) uniquement pour vous.
- Désactivez les plugins non indispensables, puis testez l’enregistrement.
2) Vérifier la mémoire et le temps d’exécution
Un fatal “sur save_post” peut être un symptôme d’un script trop lourd (génération PDF, API externe, traitement d’images). Sur l’hébergement mutualisé, ça explose au save.
- Regardez les logs :
Allowed memory size exhausted,Maximum execution time. - Vérifiez la configuration dans “Outils → Santé du site”.
Doc PHP mémoire : php.net memory_limit
3) Vérifier un conflit cache / optimisation
- Désactivez temporairement la minification JS/CSS (certains plugins injectent des scripts admin).
- Videz le cache navigateur (ou testez en navigation privée).
- Si vous utilisez un cache objet persistant (Redis/Memcached), videz-le.
4) Vérifier les permaliens (rare, mais piégeux)
Si l’enregistrement passe mais Gutenberg dit “JSON invalide”, parfois la route REST est cassée par des règles de réécriture. Allez dans “Réglages → Permaliens” et cliquez “Enregistrer” (sans changer). Ça régénère les règles.
5) Vérifier les nonces et permissions
Si vous voyez des erreurs liées à la sécurité :
- Assurez-vous que le nonce est généré et envoyé.
- Assurez-vous que votre callback vérifie
current_user_can( 'edit_post', $post_id ).
6) Vérifier que vous n’appelez pas une fonction “trop tôt”
Erreur typique : Call to undefined function acf_update_value() (ou équivalent) dans save_post. Ça arrive si le plugin n’est pas chargé dans ce contexte (ou si vous êtes sur un post type où il ne s’applique pas).
- Ajoutez un test
function_exists()avant d’appeler la fonction. - Ou déplacez la logique vers un hook plus adapté (spécifique au plugin, ou
init/rest_api_initselon le cas).
Pièges et erreurs courantes
| Symptôme / message | Cause probable | Solution recommandée |
|---|---|---|
Call to a member function ... on null |
Vous supposez qu’un objet existe (ex: champ, post, plugin) alors qu’il est null pendant autosave/REST |
Solution 1 : garde-fous + vérifier l’existence avant usage |
Undefined array key sur $_POST[...] |
Champ absent (autosave, quick edit, REST) | Tester isset() + wp_unslash() + fallback |
Allowed memory size exhausted |
Récursion, ou traitement trop lourd pendant save | Solution 2 + déplacer le traitement en tâche différée (cron/queue) |
| “Réponse JSON invalide” (éditeur de blocs) | Fatal PHP pendant la réponse REST | Lire logs + corriger le hook, vider cache, vérifier permaliens |
| Erreur après avoir “collé un snippet” | Code collé dans le mauvais fichier / parenthèse manquante / thème parent | Restaurer via FTP, coller dans thème enfant ou plugin custom, valider la syntaxe |
| Ça marche en admin mais pas via API | Dépendance à $_POST et non à la REST meta, ou vérif nonce trop stricte |
Enregistrer la meta via register_post_meta() + show_in_rest |
| Ça casse uniquement avec Divi/Elementor/Avada | Votre code touche des post types techniques du builder | Limiter par $post->post_type + conditions |
Erreurs que je vois souvent “en vrai”
- Copier le code dans
functions.phpdu thème parent, puis perdre la correction à la prochaine mise à jour. - Oublier un point-virgule : l’erreur se produit “au save” parce que ce fichier est chargé à chaque requête admin.
- Confondre action et filtre : vous utilisez
add_filterau lieu deadd_action(ou inversement), puis vous attendez un retour. - Utiliser un hook inadapté :
save_postau lieu desave_post_post(spécifique au post type “post”). - Tester directement en production sans sauvegarde, puis ne plus pouvoir accéder à l’admin.
Variante / alternative
Méthode sans code (pour blogueurs)
Si vous ne voulez pas toucher au code :
- Utilisez Health Check pour identifier le plugin fautif.
- Remplacez le snippet par un plugin de snippets plus sûr qui permet la désactivation depuis l’admin (utile si vous faites une erreur). Certains plugins de snippets offrent un “safe mode”.
- Si l’erreur vient d’un plugin premium : ouvrez un ticket support avec le stack trace complet (fichier + ligne + message). Sans ça, ils vous feront tourner.
Méthode plus avancée (développeurs) : meta enregistrée proprement (REST-friendly)
Si votre objectif est de sauvegarder des métadonnées depuis Gutenberg, évitez de dépendre de $_POST. Enregistrez la meta pour qu’elle soit gérée proprement par WordPress (validation, permissions, REST).
Où coller : plugin custom / mu-plugin.
add_action( 'init', function () {
register_post_meta(
'post',
'_mon_sous_titre',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function () {
// Autorise uniquement les utilisateurs pouvant éditer des articles.
return current_user_can( 'edit_posts' );
},
'show_in_rest' => true, // Rend la meta utilisable via l'éditeur de blocs / REST
)
);
} );
Ça ne remplace pas tous les cas (metabox custom, logique métier), mais ça réduit énormément les “fatal on save_post” liés à REST.
Éviter ce problème à l’avenir
- Utilisez
save_post_{post_type}quand c’est possible. Exemple :save_post_postne se déclenche que pour les articles. Ça évite de casser des CPT de builders. - Ajoutez toujours les garde-fous : autosave, révision, capabilities, nonce (si formulaire).
- Évitez les appels externes (API, emails, génération) dans
save_post. Déclenchez plutôt une tâche différée (cron) et loguez. - Ne modifiez pas le post dans
save_postsans anti-récursion. Si vous devez le faire, utilisezremove_actionou un verrou. - Centralisez vos snippets dans un plugin custom versionné (Git si possible). Les snippets “éparpillés” dans le thème finissent par devenir ingérables.
- Sur PHP 8.1+, traitez les warnings comme des signaux. Un
Undefined array keyaujourd’hui peut devenir un fatal demain quand une autre condition s’ajoute.
Exemple de hook plus ciblé
add_action( 'save_post_post', 'mon_save_post_article', 10, 3 );
function mon_save_post_article( $post_id, $post, $update ) {
// Même garde-fous, mais on sait déjà qu'on est sur le post type "post".
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// ...
}
Ressources
- Hook save_post (référence officielle)
- wp_is_post_autosave() et wp_is_post_revision()
- wp_verify_nonce() et Nonces (API sécurité)
- wp_update_post()
- REST API Handbook
- Health Check & Troubleshooting (plugin)
- Query Monitor (plugin)
- WordPress Core Trac (recherche de tickets liés à save_post)
- Miroir GitHub wordpress-develop (pour lire le code de wp_insert_post)
- Gestion des erreurs PHP (php.net)
Questions fréquentes
Où se trouve exactement save_post ?
save_post est une action déclenchée par le cœur WordPress pendant l’enregistrement d’un post (via wp_insert_post()). Vous ne “trouvez” pas un fichier unique : vous vous accrochez au hook avec add_action(). Référence : developer.wordpress.org.
Pourquoi l’erreur arrive seulement avec Gutenberg et pas avec l’éditeur classique ?
Gutenberg enregistre beaucoup via l’API REST. Si votre code dépend de $_POST (metabox classique), il peut ne pas trouver vos champs dans certains flux REST, et déclencher des erreurs. La solution est soit d’ajouter des garde-fous, soit d’enregistrer la meta via register_post_meta() avec show_in_rest.
Est-ce que je peux désactiver save_post ?
Non, c’est un hook central. En revanche, vous pouvez retirer votre callback avec remove_action(), ou corriger sa logique pour qu’il ne s’exécute que dans les cas voulus.
J’ai une erreur 500, mais rien dans debug.log. Pourquoi ?
Deux causes fréquentes : (1) WP_DEBUG_LOG n’est pas actif ou WordPress n’a pas le droit d’écrire dans wp-content, (2) l’hébergeur redirige les erreurs vers ses propres logs PHP-FPM. Vérifiez aussi les permissions fichiers et l’espace disque.
Que faire si je n’ai plus accès à l’admin après avoir collé un snippet ?
Connectez-vous en FTP/gestionnaire de fichiers, renommez le dossier du plugin fautif (ou retirez le code du functions.php du thème enfant). Si vous utilisiez un plugin de snippets, renommez temporairement son dossier pour le désactiver.
Pourquoi faut-il éviter de faire des appels API dans save_post ?
Parce que l’enregistrement doit rester rapide et fiable. Un appel externe lent peut provoquer un timeout, et un échec réseau peut casser la sauvegarde. Mieux : stocker une “tâche à faire” en meta et traiter via cron/queue.
Divi 5 / Elementor / Avada peuvent-ils “causer” l’erreur ?
Ils ne causent pas forcément l’erreur, mais ils multiplient les post types et les flux de sauvegarde. Un snippet mal filtré (qui s’exécute sur tous les contenus) a plus de chances de tomber sur un cas inattendu et de casser.
Quelle est la différence entre save_post et save_post_{post_type} ?
save_post se déclenche pour tous les types de contenus. save_post_post, save_post_page, etc. se déclenchent uniquement pour un type précis. Pour réduire les risques, utilisez la version spécifique quand vous le pouvez.
Je vois “nonce verification failed”. C’est grave ?
Oui : soit votre formulaire n’envoie pas le nonce, soit vous vérifiez le mauvais nom/action, soit un cache/proxy altère la requête. Sans nonce valide, n’écrivez pas en base. Corrigez la génération (wp_nonce_field()) et la vérification (wp_verify_nonce()).
Est-ce que PHP 8.1 peut “révéler” un bug qui existait déjà ?
Oui. Des pratiques tolérées sur de vieux environnements (types implicites, appels sur null, librairies obsolètes) cassent plus facilement sur PHP 8.1+. Si l’erreur est apparue après une montée de version PHP, inspectez d’abord les plugins/snippets non mis à jour.