Si vous avez déjà vu un front qui “freeze” une seconde à chaque chargement de page parce qu’un appel API externe traîne, vous avez déjà rencontré le vrai sujet derrière wp_remote_get() : la latence, les timeouts, le cache, et la gestion d’erreurs.
WordPress 6.9.4 (avril 2026) fournit une HTTP API mature, mais beaucoup de snippets circulent encore avec des anti-patterns (timeouts absurdes, absence de cache, erreurs ignorées, JSON non validé). Ici, on va faire propre, robuste, et maintenable.
Le problème / Le besoin
Vous devez consommer une API externe (météo, CRM, newsletter, IA, inventaire, données open-data) depuis WordPress, puis afficher le résultat dans une page, un widget, un bloc, ou un endpoint REST. Le besoin métier est simple : “récupérer des données et les montrer”. Le problème technique, lui, est rarement simple : l’API peut être lente, indisponible, retourner du HTML au lieu de JSON, ou vous rate-limit.
À la fin, vous saurez :
- Appeler une API externe avec
wp_remote_get()etwp_remote_post()sans bloquer inutilement le site. - Gérer proprement les erreurs (
WP_Error, codes HTTP, JSON invalide, timeouts). - Mettre en cache la réponse (transients) et éviter les appels en boucle.
- Exposer ces données via un shortcode et un endpoint REST, avec permissions et sanitization.
- Structurer le code façon “plugin” avec une mini injection de dépendances (sans framework).
Résumé rapide
- On crée un mini-plugin “API Client” basé sur la HTTP API WordPress :
wp_remote_get(),wp_remote_retrieve_response_code(),wp_remote_retrieve_body(). - On ajoute un cache applicatif via transients + verrou anti “cache stampede” (race condition quand le cache expire).
- On valide la réponse : code HTTP,
Content-Type, JSON, schéma minimal, et on journalise proprement. - On fournit deux sorties : un shortcode
[bpcab_api_demo]et un endpoint REST/wp-json/bpcab/v1/demo. - On couvre les cas réels : rate limit (429), 5xx, SSL, proxy, conflit de cache, et erreurs de configuration.
Quand utiliser cette solution
- Vous affichez des données externes sur le front (cours de bourse, disponibilité produit, avis, météo), mais vous acceptez qu’elles aient 1 à 10 minutes de “retard” via cache.
- Vous devez agréger des données externes pour un bloc/page builder (Divi 5, Elementor, Avada), et vous voulez un point d’entrée stable (shortcode ou REST).
- Vous intégrez une API avec authentification (Bearer token, API key) et vous voulez centraliser la logique (headers, retries, timeouts).
- Vous voulez éviter les appels API à chaque page vue, notamment sur des sites avec cache de page partiel ou absent.
Quand ne PAS utiliser cette solution
- Vous avez besoin de données strictement temps réel (ex: trading, stock critique) : un cache par transients peut être trop approximatif. Envisagez un worker externe, un cron dédié, ou une architecture event-driven.
- L’API doit être appelée côté navigateur (OAuth implicite, API publique CORS-friendly) : utilisez plutôt
wp_enqueue_script()+ fetch côté JS, et protégez vos secrets (ne jamais exposer une clé serveur dans le JS). - Vous devez traiter de gros volumes (pagination massive, synchronisation) : utilisez WP-CLI, Action Scheduler (plugin), ou une file de jobs. La HTTP API reste utile, mais pas “dans le rendu” d’une page.
- Vous êtes tenté d’appeler l’API dans un hook global sur chaque requête (ex:
init) : vous allez DDoS l’API et vous auto-DDoS en latence.
Prérequis / avant de commencer
- WordPress 6.9.4+ et PHP 8.1+ (recommandé). Vérifiez la version PHP dans Outils → Santé du site.
- Un environnement de staging. J’ai souvent vu ce type d’intégration casser un site en prod à cause d’un timeout trop long ou d’un endpoint qui boucle.
- Accès aux logs (error log PHP, ou un plugin de log). Pour la prod, évitez
WP_DEBUG_DISPLAY. - Une sauvegarde avant d’ajouter du code (ou au minimum un point de restauration).
Sources officielles utiles :
- HTTP API Handbook
- Référence wp_remote_get()
- REST API Handbook
- Nonces (sécurité)
- JSON en PHP (php.net)
L’approche naïve (et pourquoi l’éviter)
Voici le snippet typique croisé dans des thèmes enfants ou des plugins de snippets. Il “marche” jusqu’au jour où l’API ralentit, renvoie une erreur, ou rate-limit.
<?php
// ❌ Exemple naïf : à ne pas reproduire.
$response = wp_remote_get( 'https://api.exemple.com/data' );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
// Affiche sans contrôle, sans cache, sans gestion d'erreurs.
echo $data['title'];
Ce qui se passe en coulisses :
- Si l’API met 4 secondes, votre page met 4 secondes (ou plus) à répondre. Sur un site à trafic, ça devient vite un goulet.
- Si
wp_remote_get()renvoie unWP_Error,wp_remote_retrieve_body()renverra une chaîne vide,json_decode()renverranull, puis vous aurez des notices/Warnings, voire un fatal si vous accédez à une clé inexistante. - Vous ne vérifiez pas le code HTTP (200/401/429/500), ni le
Content-Type. Beaucoup d’APIs renvoient une page HTML d’erreur (WAF, Cloudflare) avec un code 200. - Pas de cache : chaque page vue déclenche un appel externe. Même avec un cache de page, certains contextes (admin, utilisateurs connectés, fragments) le contourneront.
- Pas de protection contre la “cache stampede” : le jour où vous ajoutez un transient naïf, 30 requêtes simultanées peuvent déclencher 30 appels au même moment quand le cache expire.
La bonne approche — tutoriel pas à pas
Étape 1 — Créez un mini-plugin (au lieu d’un snippet collé au hasard)
Évitez de coller ça dans functions.php d’un thème. J’ai vu trop de sites perdre l’intégration lors d’un changement de thème ou d’une mise à jour mal gérée. Créez plutôt un plugin.
Créez le fichier :
wp-content/plugins/bpcab-http-api-client/bpcab-http-api-client.php
Étape 2 — Concevez un client HTTP “opinionated”
Objectif : une seule fonction “haut niveau” qui :
- construit l’URL et les headers,
- applique des timeouts raisonnables,
- gère les erreurs (WP_Error + codes HTTP),
- valide/décode le JSON,
- met en cache et évite les stampedes.
Décision technique : je préfère un cache “stale-while-revalidate” simplifié. Quand le cache est expiré, on sert la dernière valeur si elle existe et on tente une mise à jour (selon contexte). Sur WordPress sans worker, on le simule avec un verrou de courte durée.
Étape 3 — Ajoutez une sortie front (shortcode) et une sortie “API” (REST)
Le shortcode simplifie l’intégration dans Divi/Elementor/Avada. Le endpoint REST sert aux cas où vous voulez un widget JS, un bloc personnalisé, ou un rendu headless.
Étape 4 — Journalisez sans polluer le front
En prod, vous voulez des logs exploitables, pas des echo. On utilisera error_log() de façon contrôlée, et on permettra un filtre pour brancher un logger plus avancé.
Code complet
Copiez-collez ce fichier complet dans wp-content/plugins/bpcab-http-api-client/bpcab-http-api-client.php, puis activez le plugin dans l’admin.
<?php
/**
* Plugin Name: BPCAB HTTP API Client (Demo)
* Description: Exemple avancé de consommation d'API externe via wp_remote_get() avec cache, gestion d'erreurs, shortcode et endpoint REST.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
* Author: BPCAB (demo)
*/
declare(strict_types=1);
if (!defined('ABSPATH')) {
exit;
}
/**
* Mini container/service locator (simple, sans framework).
* Objectif : éviter les fonctions globales partout et faciliter les tests.
*/
final class BPCAB_Container {
/** @var array<string, callable> */
private array $factories = [];
/** @var array<string, mixed> */
private array $instances = [];
public function set(string $id, callable $factory): void {
$this->factories[$id] = $factory;
}
public function get(string $id) {
if (array_key_exists($id, $this->instances)) {
return $this->instances[$id];
}
if (!isset($this->factories[$id])) {
throw new RuntimeException('Service introuvable: ' . $id);
}
$this->instances[$id] = ($this->factories[$id])($this);
return $this->instances[$id];
}
}
/**
* Client HTTP spécialisé JSON avec cache + verrou anti stampede.
*/
final class BPCAB_Http_Json_Client {
private string $base_url;
/**
* @var callable(string $message, array $context): void
*/
private $logger;
public function __construct(string $base_url, callable $logger) {
$this->base_url = rtrim($base_url, '/');
$this->logger = $logger;
}
/**
* Récupère un endpoint JSON (GET) avec cache.
*
* @param string $path Exemple: '/v1/demo'
* @param array $query Query args (seront encodés).
* @param array $args Options: timeout, ttl, headers, auth_bearer, cache_key_extra
* @return array{ok:bool, code:int, data:mixed, error:string|null, from_cache:bool}
*/
public function get_json(string $path, array $query = [], array $args = []): array {
$timeout = isset($args['timeout']) ? (float) $args['timeout'] : 4.0;
$ttl = isset($args['ttl']) ? (int) $args['ttl'] : 300;
// Timeout trop élevé = threads PHP bloqués. En pratique, 3-6s est un bon plafond.
if ($timeout <= 0 || $timeout > 10) {
$timeout = 4.0;
}
if ($ttl < 0) {
$ttl = 0;
}
$url = $this->base_url . '/' . ltrim($path, '/');
if (!empty($query)) {
$url = add_query_arg(array_map('strval', $query), $url);
}
$headers = [
'Accept' => 'application/json',
];
// Headers custom.
if (!empty($args['headers']) && is_array($args['headers'])) {
foreach ($args['headers'] as $k => $v) {
if (is_string($k) && (is_string($v) || is_numeric($v))) {
$headers[$k] = (string) $v;
}
}
}
// Auth Bearer (ne jamais exposer côté front si c'est un secret serveur).
if (!empty($args['auth_bearer']) && is_string($args['auth_bearer'])) {
$headers['Authorization'] = 'Bearer ' . $args['auth_bearer'];
}
$cache_key_extra = '';
if (!empty($args['cache_key_extra']) && is_string($args['cache_key_extra'])) {
$cache_key_extra = $args['cache_key_extra'];
}
$cache_key = $this->cache_key('get_json', $url, $headers, $cache_key_extra);
// 1) Cache hit.
$cached = get_transient($cache_key);
if (is_array($cached) && isset($cached['ok'])) {
$cached['from_cache'] = true;
return $cached;
}
// 2) Anti stampede lock (verrou court).
$lock_key = $cache_key . ':lock';
if (!$this->acquire_lock($lock_key, 20)) {
// Quelqu'un d'autre est en train de rafraîchir.
// Si on a un "stale" (fallback), on le sert.
$stale = get_transient($cache_key . ':stale');
if (is_array($stale) && isset($stale['ok'])) {
$stale['from_cache'] = true;
$stale['error'] = $stale['error'] ?? 'Servi depuis cache stale (refresh en cours).';
return $stale;
}
// Pas de stale : on tente quand même un call, mais on met un timeout plus agressif.
$timeout = min($timeout, 2.0);
}
// 3) Call HTTP.
$request_args = [
'method' => 'GET',
'timeout' => $timeout,
'redirection' => 3,
'httpversion' => '1.1',
'headers' => $headers,
'compress' => true,
'decompress' => true,
'reject_unsafe_urls' => true,
// 'sslverify' => true, // true par défaut. Ne le désactivez pas "pour tester" en prod.
];
/**
* Filtre pour ajuster les args WP_HTTP par endpoint.
* Utile si vous avez un proxy, un certificat spécifique, etc.
*/
$request_args = apply_filters('bpcab_http_client_request_args', $request_args, $url, $args);
$response = wp_remote_get($url, $request_args);
$result = [
'ok' => false,
'code' => 0,
'data' => null,
'error' => null,
'from_cache' => false,
];
if (is_wp_error($response)) {
$result['error'] = $response->get_error_message();
($this->logger)('HTTP error (WP_Error)', [
'url' => $url,
'error' => $response->get_error_code(),
'message' => $response->get_error_message(),
]);
$this->store_cache($cache_key, $result, $ttl);
$this->release_lock($lock_key);
return $result;
}
$code = (int) wp_remote_retrieve_response_code($response);
$result['code'] = $code;
$body = (string) wp_remote_retrieve_body($response);
// Content-Type : on vérifie sans être trop strict (charset, vendor types).
$content_type = '';
$headers_resp = wp_remote_retrieve_headers($response);
if (is_array($headers_resp) && isset($headers_resp['content-type'])) {
$content_type = (string) $headers_resp['content-type'];
} elseif (is_object($headers_resp) && method_exists($headers_resp, 'getAll')) {
$all = $headers_resp->getAll();
if (isset($all['content-type'])) {
$content_type = (string) $all['content-type'];
}
}
// Gestion codes HTTP.
if ($code < 200 || $code >= 300) {
// 429 : rate limit fréquent.
$result['error'] = 'Réponse HTTP non-OK: ' . $code;
($this->logger)('HTTP non-OK', [
'url' => $url,
'code' => $code,
'body_excerpt' => substr($body, 0, 500),
]);
// Cache court sur erreurs serveur pour éviter le martelage.
$error_ttl = ($code === 429) ? 60 : 30;
$this->store_cache($cache_key, $result, min($ttl, $error_ttl));
$this->release_lock($lock_key);
return $result;
}
// Vérification Content-Type (optionnelle mais utile).
if ($content_type !== '' && stripos($content_type, 'json') === false) {
$result['error'] = 'Content-Type inattendu: ' . $content_type;
($this->logger)('Unexpected Content-Type', [
'url' => $url,
'content_type' => $content_type,
'body_excerpt' => substr($body, 0, 500),
]);
$this->store_cache($cache_key, $result, min($ttl, 60));
$this->release_lock($lock_key);
return $result;
}
// Decode JSON avec détection d'erreur fiable.
$data = json_decode($body, true);
if (!is_array($data) && !is_object($data)) {
$result['error'] = 'JSON invalide (decode échoué).';
($this->logger)('JSON decode failed', [
'url' => $url,
'json_error' => function_exists('json_last_error_msg') ? json_last_error_msg() : 'unknown',
'body_excerpt' => substr($body, 0, 500),
]);
$this->store_cache($cache_key, $result, min($ttl, 60));
$this->release_lock($lock_key);
return $result;
}
// Exemple de validation minimale (adapter à votre API).
// Ici, on exige au moins une clé "title" si c'est un array.
if (is_array($data) && !array_key_exists('title', $data)) {
($this->logger)('JSON schema mismatch (demo rule)', [
'url' => $url,
'keys' => implode(',', array_slice(array_keys($data), 0, 20)),
]);
// On ne bloque pas forcément : dépend du contrat API.
}
$result['ok'] = true;
$result['data'] = $data;
$this->store_cache($cache_key, $result, $ttl);
// On conserve une copie "stale" plus longue pour fallback.
$this->store_cache($cache_key . ':stale', $result, max($ttl * 6, 1800));
$this->release_lock($lock_key);
return $result;
}
private function cache_key(string $prefix, string $url, array $headers, string $extra): string {
// On évite de mettre des secrets en clair dans la clé.
$headers_sanitized = $headers;
if (isset($headers_sanitized['Authorization'])) {
$headers_sanitized['Authorization'] = 'Bearer ***';
}
$hash = hash('sha256', $prefix . '|' . $url . '|' . wp_json_encode($headers_sanitized) . '|' . $extra);
return 'bpcab_http_' . substr($hash, 0, 40);
}
private function store_cache(string $key, array $value, int $ttl): void {
if ($ttl === 0) {
return;
}
set_transient($key, $value, $ttl);
}
private function acquire_lock(string $key, int $ttl): bool {
// add_option est atomique au niveau DB : utile pour un verrou simple.
// On utilise un autoload = no.
$now = time();
$lock = [
'ts' => $now,
'ttl' => $ttl,
];
$added = add_option($key, $lock, '', 'no');
if ($added) {
return true;
}
$existing = get_option($key);
if (is_array($existing) && isset($existing['ts'], $existing['ttl'])) {
$expires = (int) $existing['ts'] + (int) $existing['ttl'];
if ($expires < $now) {
// Verrou expiré : on le remplace.
update_option($key, $lock, 'no');
return true;
}
}
return false;
}
private function release_lock(string $key): void {
// Nettoyage best-effort.
delete_option($key);
}
}
/**
* Plugin principal : enregistre shortcode + REST route.
*/
final class BPCAB_Http_Api_Plugin {
private BPCAB_Container $container;
public function __construct() {
$this->container = new BPCAB_Container();
$this->container->set('logger', function () {
return function (string $message, array $context = []): void {
/**
* Filtre : branchez votre logger (Monolog, Sentry, etc.).
* Retour attendu : callable(string $message, array $context): void
*/
$callable = apply_filters('bpcab_http_client_logger', null);
if (is_callable($callable)) {
$callable($message, $context);
return;
}
// Fallback simple : error_log. Évitez de logger des secrets.
error_log('[BPCAB HTTP] ' . $message . ' ' . wp_json_encode($context));
};
});
$this->container->set('client', function (BPCAB_Container $c) {
$logger = $c->get('logger');
/**
* Base URL de démo : remplacez par votre API.
* Pour tester facilement, vous pouvez utiliser https://httpbin.org/json (renvoie un JSON stable).
*/
$base_url = apply_filters('bpcab_http_client_base_url', 'https://httpbin.org');
return new BPCAB_Http_Json_Client($base_url, $logger);
});
}
public function hooks(): void {
add_shortcode('bpcab_api_demo', [$this, 'shortcode_demo']);
add_action('rest_api_init', function () {
register_rest_route('bpcab/v1', '/demo', [
'methods' => 'GET',
'callback' => [$this, 'rest_demo'],
'permission_callback' => '__return_true',
'args' => [
'refresh' => [
'description' => 'Force le refresh (réservé aux admins).',
'type' => 'boolean',
'required' => false,
],
],
]);
});
}
public function shortcode_demo(array $atts = []): string {
$atts = shortcode_atts([
'ttl' => '300',
], $atts, 'bpcab_api_demo');
$ttl = max(0, (int) $atts['ttl']);
$client = $this->container->get('client');
// Démo httpbin : /json renvoie { slideshow: { title: ... } }
$result = $client->get_json('/json', [], [
'timeout' => 4,
'ttl' => $ttl,
// 'auth_bearer' => 'votre-token', // exemple.
]);
if (!$result['ok']) {
// Escaping strict : on n'affiche pas de raw.
$msg = $result['error'] ? esc_html($result['error']) : 'Erreur inconnue';
return '<div class="bpcab-api-demo bpcab-api-demo--error">API indisponible : ' . $msg . '</div>';
}
$title = '';
if (is_array($result['data']) && isset($result['data']['slideshow']['title'])) {
$title = (string) $result['data']['slideshow']['title'];
}
$meta = $result['from_cache'] ? 'cache' : 'live';
return '<div class="bpcab-api-demo">Titre API : <strong>' . esc_html($title) . '</strong> <em>(' . esc_html($meta) . ')</em></div>';
}
public function rest_demo(WP_REST_Request $request): WP_REST_Response {
$refresh = (bool) $request->get_param('refresh');
// Refresh réservé aux admins : évite que n'importe qui force des appels externes (DoS).
if ($refresh && !current_user_can('manage_options')) {
return new WP_REST_Response([
'ok' => false,
'error' => 'refresh interdit',
], 403);
}
$client = $this->container->get('client');
// Si refresh, on met ttl=0 temporairement pour bypass cache.
$ttl = $refresh ? 0 : 300;
$result = $client->get_json('/json', [], [
'timeout' => 4,
'ttl' => $ttl,
'cache_key_extra' => $refresh ? 'refresh' : '',
]);
$status = $result['ok'] ? 200 : 502;
return new WP_REST_Response([
'ok' => $result['ok'],
'code' => $result['code'],
'from_cache' => $result['from_cache'],
'data' => $result['ok'] ? $result['data'] : null,
'error' => $result['ok'] ? null : $result['error'],
], $status);
}
}
add_action('plugins_loaded', function () {
$plugin = new BPCAB_Http_Api_Plugin();
$plugin->hooks();
});
Explication du code
Architecture (pourquoi ce découpage)
Le cœur est BPCAB_Http_Json_Client : une API stable qui renvoie toujours une structure [ok, code, data, error, from_cache]. C’est volontaire : côté appelant, vous évitez les try/catch partout et vous centralisez les décisions (timeouts, cache, validation).
Le “container” est minimaliste. Je l’utilise ici pour éviter de créer 15 singletons globaux. Sur des projets plus gros, vous remplacerez ça par votre container maison, ou une approche “services” plus stricte.
HTTP API : points clés
- Timeout : plafonné à 10 secondes. Au-delà, vous bloquez PHP-FPM/Apache inutilement. Dans mon expérience, 3–6 secondes est un bon compromis pour un front.
- Headers :
Accept: application/jsonpar défaut. On supporte aussi un Bearer token (serveur uniquement). - reject_unsafe_urls : utile contre certaines URL “exotiques”. Ne désactivez pas ça sans raison.
- compress/decompress : améliore souvent les perfs si l’API supporte gzip.
Références : wp_remote_get() et HTTP API Handbook.
Gestion d’erreurs : WP_Error + codes HTTP + JSON
Trois étages :
- Transport :
is_wp_error($response)(DNS, SSL, timeout, connexion). - Protocole : code HTTP non-2xx (401/403/404/429/500). On cache court les erreurs pour éviter de marteler.
- Contenu :
Content-Typepas JSON, JSON invalide, schéma inattendu.
Le piège classique : ignorer le code HTTP et tenter json_decode() sur une page HTML d’erreur. Vous obtenez null, puis des erreurs aval.
Cache : transients + “stale” + verrou
Le cache est en transients, donc compatible avec l’object cache si présent (Redis/Memcached). On stocke deux clés :
- clé principale : TTL normal (ex: 300s)
- clé stale : TTL plus long (ex: 1800s+) pour fallback
Le verrou via add_option() réduit le risque de “stampede” quand le cache expire sous charge. Sans ça, 20 requêtes simultanées déclenchent 20 appels externes. Sur une API rate-limitée, c’est un incident garanti.
REST : refresh protégé
Le paramètre refresh=1 est pratique en debug, mais dangereux si public. Ici, il est limité à manage_options. Sinon, n’importe qui peut forcer des appels externes, ce qui ressemble à un DoS applicatif.
Référence : Ajouter des endpoints REST.
Variantes et cas d’usage
Variante 1 — POST JSON (webhook sortant, création de ressource)
Pour un POST, gardez la même discipline : timeouts, headers, validation, et ne loggez jamais le payload complet si ça contient des PII.
<?php
// Exemple : à intégrer dans la classe client si vous en avez besoin.
$payload = [
'email' => '[email protected]',
'tags' => ['wp', 'demo'],
];
$args = [
'method' => 'POST',
'timeout' => 6,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json; charset=utf-8',
'Authorization' => 'Bearer ' . $token,
],
'body' => wp_json_encode($payload),
];
$response = wp_remote_post('https://api.exemple.com/v1/subscribe', $args);
if (is_wp_error($response)) {
// ...
}
Référence : wp_remote_post().
Variante 2 — ETag / If-None-Match (cache HTTP “propre”)
Si l’API renvoie un ETag, vous pouvez stocker l’ETag en transient et envoyer If-None-Match. Si l’API répond 304 Not Modified, vous réutilisez le cache sans re-télécharger le body. C’est plus élégant, mais toutes les APIs ne le supportent pas.
En pratique, je le fais surtout sur des APIs très appelées, avec bodies lourds.
Variante 3 — Préchargement via WP-Cron (éviter l’appel pendant le rendu)
Si l’appel est coûteux, déclenchez-le via cron et servez uniquement le cache sur le front. Attention : WP-Cron dépend du trafic. Sur un site faible trafic, utilisez un cron système.
<?php
// Exemple minimal : planifier un refresh périodique.
add_action('init', function () {
if (!wp_next_scheduled('bpcab_refresh_api_cache')) {
wp_schedule_event(time() + 60, 'five_minutes', 'bpcab_refresh_api_cache');
}
});
add_filter('cron_schedules', function ($schedules) {
$schedules['five_minutes'] = [
'interval' => 300,
'display' => 'Toutes les 5 minutes',
];
return $schedules;
});
add_action('bpcab_refresh_api_cache', function () {
// Ici, appelez votre client avec ttl=300.
// L'objectif : remplir le cache en amont.
});
Compatibilité Divi 5 / Elementor / Avada
Divi 5
Le plus stable : insérer le shortcode [bpcab_api_demo ttl="300"] via un module “Code” ou “Texte”. Divi 5 gère généralement bien les shortcodes, mais j’ai déjà vu des mises en cache agressives côté builder : si vous ne voyez pas les changements, videz le cache Divi et le cache serveur.
Si vous développez un module Divi 5 custom, gardez l’appel API hors du rendu React côté builder : consommez plutôt l’endpoint REST /wp-json/bpcab/v1/demo et mettez un cache côté serveur comme ici.
Elementor
Le widget “Shortcode” est le chemin le plus court. Pour un widget custom, préférez :
- server-side : shortcode ou rendu PHP,
- client-side : REST + fetch, en évitant de mettre des secrets dans le JS.
Edge case fréquent : Elementor peut prévisualiser en mode éditeur avec un contexte utilisateur connecté, ce qui change la présence de caches de page. Ne concluez pas trop vite que “le cache ne marche pas” : testez en navigation privée.
Avada (Fusion Builder)
Avada accepte les shortcodes dans ses éléments texte/code selon configuration. Si l’affichage est “nettoyé” (HTML filtré), utilisez un élément qui autorise explicitement les shortcodes.
Avada a aussi ses propres caches/compilations. Après ajout du plugin, faites un purge côté Avada si vous ne voyez rien.
Vérifications après mise en place
- Activez le plugin, puis ajoutez
[bpcab_api_demo]dans une page. - Chargez la page deux fois :
- 1er chargement : devrait être live
- 2e chargement : devrait passer en cache
- Testez l’endpoint REST : ouvrez
/wp-json/bpcab/v1/demodans le navigateur. - Testez
/wp-json/bpcab/v1/demo?refresh=1connecté en admin (sinon 403). - Regardez vos logs : vous ne devriez pas avoir de notices PHP.
Tableau de diagnostic (symptômes réels)
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| La page met 5–10s à charger | Timeout trop élevé, API lente, pas de cache effectif | Inspectez timeout, testez l’URL API en curl |
Réduisez timeout, ajoutez/validez transients, préchargez via cron |
| “API indisponible : cURL error 28” | Timeout transport (DNS/SSL/latence) | Logs PHP, test curl -I, vérifier firewall |
Augmentez légèrement le timeout (ex: 6s), corrigez DNS/SSL, autorisez la sortie |
| Réponse vide / JSON invalide | L’API renvoie HTML (WAF/Cloudflare) ou 200 avec page d’erreur | Log “Unexpected Content-Type”, inspectez body_excerpt | Fixer endpoint, ajouter headers requis, gérer auth, whitelister IP |
| 429 Too Many Requests | Rate limit API, cache absent, stampede | Code HTTP = 429 dans logs | Augmenter TTL, ajouter verrou (déjà fait), précharger via cron |
| Ça marche en admin mais pas sur le front | Cache de page, CDN, ou builder qui met en cache | Test navigation privée, purges caches | Purger caches (plugin/CDN), vérifier variation par cookie/utilisateur |
Si ça ne marche pas
- Vérifiez où vous avez mis le code. Si vous l’avez collé dans un plugin de snippets, un seul caractère manquant (point-virgule) peut casser l’exécution. Le plugin dédié réduit ce risque.
- Activez le debug sur staging :
WP_DEBUG+ log, sans display. Vérifiezwp-content/debug.logsi configuré. - Testez l’API hors WordPress :
curl -i https://httpbin.org/json - Regardez les erreurs réseau : proxy sortant, DNS, IPv6 cassé, certificat SSL. Les erreurs cURL sont fréquentes sur des hébergements mutualisés verrouillés.
- Désactivez temporairement les caches (CDN, cache plugin) pour valider le comportement live vs cache.
- Conflits de hooks : si vous avez déplacé l’enregistrement REST hors de
rest_api_init, il peut ne pas être chargé au bon moment. - Version PHP trop ancienne : ce code cible PHP 8.1+. Sur PHP 7.4, vous aurez des erreurs (typed properties, strict_types). Mettez à niveau.
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
| Écran blanc après activation | Erreur PHP (parenthèse, point-virgule, classe dupliquée) | Consultez les logs, désactivez le plugin via FTP, corrigez la syntaxe |
Call to undefined function wp_remote_get() |
Code exécuté hors contexte WordPress (fichier appelé directement) | Gardez if (!defined('ABSPATH')) exit; et chargez via plugin |
| Le shortcode affiche toujours “live” | Transients non persistants (object cache instable) ou TTL=0 | Vérifiez ttl, testez get_transient(), inspectez cache serveur |
| Appels API en boucle sur chaque page | Cache key différente à chaque fois (query variable, header variable) | Stabilisez les paramètres, évitez d’inclure des valeurs volatiles dans la clé |
| “JSON invalide” alors que l’API marche | Compression/proxy qui injecte du contenu, ou réponse tronquée | Logguez un extrait, vérifiez Content-Length, testez sans proxy |
Erreur 403 sur ?refresh=1 |
Permissions : pas admin | Connectez-vous avec un compte ayant manage_options ou retirez refresh en prod |
| Le code d’un ancien tutoriel ne marche plus | Snippets obsolètes (désactivation SSL verify, mauvais headers, pas de gestion WP_Error) | Revenez aux fonctions HTTP API actuelles et aux patterns de cache/validation ci-dessus |
| Conflit avec un plugin de cache | Page cache sert une version figée du HTML | Utilisez REST côté JS ou configurez des exclusions, ou acceptez la durée de cache |
Conseils sécurité, performance et maintenance
- Ne désactivez pas SSL verify pour “faire marcher”. Si vous mettez
'sslverify' => false, vous ouvrez la porte à des attaques MITM. Corrigez le certificat ou la chaîne CA côté serveur. - Protégez les secrets : API keys et tokens doivent être stockés côté serveur (constants, options chiffrées si vous avez un mécanisme, variables d’environnement). Ne les mettez jamais dans un shortcode ou dans du JS.
- Cachez intelligemment : un TTL trop bas = rate limit. Un TTL trop haut = données obsolètes. Ajustez par endpoint.
- Évitez le rendu bloquant : si c’est critique, préchargez via cron et servez uniquement le cache au front.
- Observez : loggez les codes 429/5xx, mesurez la latence, et mettez des alertes. Sur des APIs tierces, le “ça marchait hier” est la norme.
- Backward compatibility : le code est compatible WP 6.9.4+ / PHP 8.1+. Si vous devez supporter plus bas, vous devrez supprimer
declare(strict_types=1), typed properties, et ajuster.
Pour aller plus loin côté core, suivez les évolutions de la HTTP API sur Trac : core.trac.wordpress.org (recherchez “HTTP API”, “Requests”, “wp_remote_get”). Le moteur sous-jacent repose historiquement sur la librairie Requests ; côté GitHub, vous trouverez des discussions dans le miroir : wordpress-develop.
Ressources
- HTTP API Handbook (developer.wordpress.org)
- wp_remote_get() (référence)
- wp_remote_retrieve_response_code() (référence)
- wp_remote_retrieve_body() (référence)
- REST API : endpoints custom
- Nonces WordPress (sécurité)
- WordPress Core Trac (tickets)
- GitHub wordpress-develop (miroir)
- json_decode() (php.net)
FAQ
Quel timeout choisir pour wp_remote_get() ?
Pour du rendu front, je reste généralement entre 3 et 6 secondes. Au-delà, vous bloquez des workers PHP. Si vous avez besoin de plus, sortez l’appel du rendu (cron, job queue) et servez un cache.
Pourquoi mon appel marche en local mais pas en production ?
Souvent : firewall sortant, DNS différent, IPv6 instable, ou certificat SSL intercepté. Les erreurs cURL error 28, could not resolve host, SSL certificate problem sont typiques.
Est-ce que je peux utiliser file_get_contents() à la place ?
Techniquement oui, mais vous perdez l’intégration WordPress (proxy, SSL config, hooks, compat). La HTTP API est conçue pour fonctionner dans l’écosystème WP. Je réserve file_get_contents() à des scripts hors WP, pas à un plugin.
Pourquoi vérifier le Content-Type si le code HTTP est 200 ?
Parce que certaines plateformes renvoient une page HTML d’erreur avec un 200 (WAF, pages de challenge, erreurs applicatives). Sans check, vous tentez de décoder du HTML en JSON.
Comment éviter que mon site “martèle” l’API à l’expiration du cache ?
Deux mesures : TTL raisonnable + verrou anti stampede. Le verrou évite le pic d’appels simultanés. Le cache stale permet de servir une valeur même si le refresh est en cours.
Où stocker une clé API ?
Idéalement en variable d’environnement (si votre infra le permet) ou dans wp-config.php (constant). Évitez de la stocker en clair dans la base si vous n’avez pas de stratégie de rotation/accès. Ne l’exposez jamais côté navigateur.
Comment tester rapidement sans dépendre d’une vraie API ?
Utilisez un endpoint stable comme https://httpbin.org/json (comme dans le code). Pour simuler des erreurs, httpbin propose aussi des endpoints de status (ex: /status/500), mais adaptez le code de démo si vous changez de route.
Pourquoi renvoyer 502 dans l’endpoint REST quand l’API externe échoue ?
502 (“Bad Gateway”) décrit bien “mon serveur agit comme passerelle vers un service amont qui a échoué”. Ça aide aussi vos clients (JS, monitoring) à classifier l’erreur.
Puis-je appeler l’API dans init pour “précharger” ?
Évitez. init s’exécute sur presque toutes les requêtes, y compris admin, AJAX, REST. Préférez un cron, ou un mécanisme déclenché explicitement (admin action sécurisée) si vous devez précharger.
Comment brancher un logger plus propre que error_log() ?
Utilisez le filtre bpcab_http_client_logger et retournez un callable. Exemple : envoyer vers un service de logs, ou vers un fichier dédié. Ne loggez pas les headers d’auth ni des données personnelles.