Pourquoi un endpoint REST protégé renvoie-t-il systématiquement 403, même quand le visiteur n’est tout simplement pas connecté ? C’est la question qui revient dès qu’on regarde de près le comportement par défaut de l’API REST de WordPress sur les routes qui exigent une authentification.
La nuance a pourtant un sens précis dans la spécification HTTP : 401 (Unauthorized) signifie « je ne sais pas qui vous êtes », tandis que 403 (Forbidden) signifie « je sais qui vous êtes, et vous n’avez pas le droit ». Confondre les deux complique le travail de tout client qui essaie de réagir intelligemment à une erreur, par exemple en proposant un écran de connexion plutôt qu’un simple message bloquant.
Le comportement par défaut de l’API REST
Par défaut, quand un permission_callback renvoie false ou un objet WP_Error sans code HTTP explicite, WordPress applique une règle unique : il renvoie 401 si l’utilisateur n’est pas connecté, et 403 s’il l’est. Cette logique est centralisée dans le filtre rest_authorization_required_code, disponible dans WP_REST_Server.
Le souci vient rarement du cœur lui-même, mais de la façon dont les développeurs codent leurs permission_callback : beaucoup renvoient un simple false booléen ou une erreur générique sans distinguer les deux cas, ce qui aplatit la nuance avant même que le filtre n’entre en jeu.
Écrire un permission_callback qui distingue les deux cas

La bonne pratique consiste à vérifier explicitement l’état de connexion avant de statuer sur l’autorisation :
register_rest_route( 'boutique/v1', '/factures/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'boutique_get_facture',
'permission_callback' => function ( $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
'Vous devez être connecté pour consulter cette facture.',
array( 'status' => 401 )
);
}
if ( ! current_user_can( 'read_facture', $request['id'] ) ) {
return new WP_Error(
'rest_forbidden',
"Vous n'avez pas accès à cette facture.",
array( 'status' => 403 )
);
}
return true;
},
) );
En précisant le statut directement dans les données de l’erreur, on court-circuite la logique par défaut du filtre pour ce cas précis : WordPress respecte le statut explicitement fourni plutôt que d’appliquer sa règle générique.
Utiliser le filtre pour un comportement global
Quand la distinction doit s’appliquer à toutes les routes d’un site, plutôt que de retoucher chaque permission_callback une par une, on peut ajuster directement le filtre :
add_filter( 'rest_authorization_required_code', function ( $code ) {
if ( ! is_user_logged_in() ) {
return 401;
}
return 403;
} );
Ce filtre reçoit en argument le code par défaut calculé par WordPress et permet de le remplacer selon un contexte plus large, par exemple en tenant compte d’un rôle ou d’une capacité spécifique au projet.
Ce que change la distinction pour le client
- Un client JavaScript peut rediriger vers l’écran de connexion sur un 401, sans afficher de message d’erreur alarmant.
- Un 403 peut déclencher un message explicite du type « contactez votre administrateur », plus adapté qu’une simple invitation à se reconnecter.
- Les outils de supervision et les journaux d’accès distinguent mieux les tentatives d’intrusion des simples sessions expirées.
Sur une application qui consomme l’API REST depuis un frontal découplé, cette distinction évite des heures de débogage côté client, là où un développeur cherche pourquoi un utilisateur pourtant connecté se retrouve renvoyé vers la page de connexion.
Le cas des routes publiques avec restriction partielle
Un cas plus subtil concerne les routes accessibles à tous mais dont certains champs sont restreints selon les droits. Dans ce contexte, on ne renvoie jamais une erreur globale : on filtre plutôt la réponse elle-même, en retirant les champs sensibles pour les visiteurs non autorisés plutôt qu’en bloquant toute la requête. La logique 401/403 ne s’applique alors qu’aux routes réellement fermées.
Sur une API destinée à être consommée par un client tiers, documenter explicitement quel code HTTP correspond à quelle situation fait gagner un temps précieux à l’équipe qui l’intègre.
En résumé
Le réflexe à adopter est simple : vérifier is_user_logged_in() avant de statuer sur les permissions, et laisser le filtre rest_authorization_required_code gérer les cas non explicités. Cette petite discipline suffit à transformer une API REST approximative en une API dont chaque code HTTP raconte fidèlement ce qui s’est passé.