Général

Les PCRE

Les POSIX

Pratique

Linux

Spécial php

Les billets de fred

Headers already sent : j'ai perdu la tête

Les messages d'erreur "Cannot modify header information - headers already sent by ..." et "Cannot send session cookie - headers already sent by ..." sont des grands classiques du développement PHP, mais beaucoup ne comprennent pas exactement toutes les raisons de ce type d'anomalie.

Qu'est qu'un en-tête ?

En fait, dans le monde du développement Web, quand on parle d'en-têtes, on désigne les en-têtes du protocole HTTP. HTTP, c'est le langage pratiqué par votre navigateur et le serveur Web. Dans leurs échanges, le navigateur et le serveur Web vont faire précéder leur contenu d'une série d'informations. C'est une sorte d'avant propos que l'on appelle l'en-tête. C'est d'ailleurs un abus de langage que de parler d'en-tête au pluriel car l'en-tête est juste la partie qui précède le contenu et il n'y en a pas plusieurs.

Que contient l'en-tête ?

Eh bien, cela dépend s'il provient du navigateur ou du serveur Web !

Dans le cas du navigateur, il y a quelques informations qui sont obligatoires telles que la page demandée, ce qui est bien utile pour que le serveur sache quel contenu retourner, mais on peut retrouver d'autres informations telles que le type de navigateur, l'URL de la page précédente si elle existe, le type d'encodage des caractères supportés, etc.

Si le navigateur dispose de cookies associés au site Web visité, c'est également au travers de l'en-tête qu'il va communiquer cette information.

En ce qui concerne le serveur Web, celui-ci va également communiquer différentes informations telles que le type de contenu envoyé, c'est-à-dire HTML, texte, image GIF, animation Flash, document PDF, etc. C'est également dans l'en-tête que le serveur Web indique au navigateur le contenu des cookies à enregistrer ou la page à laquelle il doit désormais se rendre.

Comment peut-on voir ces informations d'en-tête ?

Les informations envoyées par le navigateur se retrouvent dans la variable super-globale $_SERVER sous des noms préfixés par "HTTP_", mais vous pouvez aussi consulter ces données au moyen de la fonction " apache_request_headers()" si PHP est utilisé comme module Apache.

Pour les informations envoyées par le serveur Web, vous disposez de la fonction " apache_response_headers()", mais j'ai constaté que celle-ci ne retournait pas toujours toutes les informations d'en-tête. Ainsi, le "content-type" ne figure pas dans cette liste. Si PHP n'est pas installé comme module Apache, cette fonction n'est pas utilisable et il n'est donc pas possible d'obtenir ces informations.

Si vous exécutez le code suivant :
<?php
error_reporting(E_ALL);
header('content-type: text/plain', true);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('X-Mon-En-Tete: ma valeur');

setcookie( 'membre', 'charlie', time() + 100 );

print_r( apache_response_headers() );
echo "\n------------\n";
print_r( apache_request_headers() );
?>

Vous obtiendrez une réponse dans le genre :
Array
(
    [X-Powered-By] => PHP/5.0.3
    [Last-Modified] => Thu, 31 Mar 2005 16:43:58 GMT
    [Cache-Control] => no-store, no-cache, must-revalidate
    [X-Mon-En-Tete] => ma valeur
    [Set-Cookie] => membre=charlie; expires=Thu, 31-Mar-2005 16:45:38 GMT
)

------------
Array
(
    [Host] => test1
    [User-Agent] => Mozilla/5.0 (Windows; U; Windows NT 5.0; fr-FR; rv:1.7.6) Gecko/20050223 Firefox/1.0.1
    [Accept] => text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,
text/plain;q=0.8,image/png,*/*;q=0.5
    [Accept-Language] => fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3
    [Accept-Encoding] => gzip,deflate
    [Accept-Charset] => ISO-8859-1,utf-8;q=0.7,*;q=0.7
    [Keep-Alive] => 300
    [Connection] => keep-alive
    [Cache-Control] => max-age=0
)

Remarque : Toujours pour les heureux utilisateurs de Firefox, il existe une excellente extension nommée LiveHTTPHeaders permettant d'afficher tous les en-têtes échangés entre votre butineur et le site Web.

Et l'erreur headers already sent ?

Comme je vous l'ai dit, l'en-tête est envoyé avant le contenu, or le contenu, c'est le texte placé en dehors des balises PHP ou généré par des fonctions comme "echo", "printf", "print", "print_r", "var_dump" etc.

Vous comprendrez bien que le code suivant pose un problème à PHP :
<?php
echo 'Mon texte' ;
setcookie('membre', 'charlie', time() + 100 );
?>

En effet, l'information sur le cookie est envoyée dans l'en-tête, or du contenu a déjà été généré et vous obtenez la fameuse erreur "Cannot modify header information - headers already sent".

Quand envoyons-nous des informations d'en-tête ?

Soit en utilisant directement la fonction " header" prévue à cet effet, soit comme nous l'avons vu plus haut, lorsque nous envoyons des cookies. Mais il y a un autre cas, c'est au moment de l'appel à la fonction session_start. En effet, dans sa configuration par défaut, l'utilisation des sessions entraîne la sauvegarde du SID dans un cookie et nous pouvons constater le message d'erreur un peu plus explicite : "Cannot send session cookie - headers already sent".

Si on veut pouvoir utiliser le système de session, il est donc impératif de placer la fonction session_start avant de générer le moindre contenu.

Cependant, il n'est pas rare de voir des gens s'interroger sur le fait qu'ils n'utilisent pas de echo/print, mais qu'ils obtiennent malgré tout cette erreur. Pourtant, ce message est sans équivoque, ils envoient obligatoirement du contenu et d'ailleurs, s'ils lisaient les messages d'erreur en entier, ils constateraient que PHP leur indique l'emplacement exact du contenu perturbateur.

Généralement, ce contenu correspond à du texte placé en dehors des balises PHP, comme ceci :

<?php
session_start() ;
?>

Comme vous pouvez le constater, il y a un retour à la ligne juste avant la balise PHP, ce qui implique l'envoi d'un contenu sous la forme d'un caractère invisible.
Le code le plus pernicieux est sans conteste celui-ci :
<?php
require 'config.inc.php' ;
session_start() ;
?>

Il ne semble pas y avoir de caractères invisibles en dehors des balises PHP, mais en regardant le message d'erreur avec plus d'attention comme nous devrions le faire plus souvent, nous pouvons lire : "output started at /www/config.inc.php:27". Ce message nous indique donc, que l'envoi de contenu a commencé dans le script config.inc.php à la ligne 27 et il y a de fortes chances pour que cette ligne corresponde à la balise PHP de fermeture. En observant de plus près, nous allons constater un simple espace juste après cette balise, et cet insignifiant caractère invisible est le grain de sable dans l'engrenage. Dans les fichiers inclus, il ne doit donc jamais y avoir d'espaces ou de retours à la ligne avant et après les balises PHP.

Que faire si on veut vraiment sortir du texte avant ?

Il y a des possibilités comme rediriger temporairement la sortie vers la mémoire au moyen de la fonction " ob_start" ou en modifiant la directive de configuration "output_buffering" pour temporiser cette sortie (voir article sur "echo" : Lapin ou tortue).

Mais il faut se poser une autre question : Pourquoi vouloir absolument générer du contenu avant d'envoyer les en-têtes ?
Ma réponse est sans appel : vous n'avez aucune raison de faire une telle chose car cela relève d'une erreur de conception !

Le plus souvent, cela vient de l'utilisation de concepts tel que les pseudo-frames, dont je déteste l'appellation :
<html>
<head>
<title>Mon super site</title>
<body>
<?php require 'menu.php' ?>
<div id="contenu">
<?php
$page = isset( $_GET['p'] ) ? basename($_GET['p']) : 'accueil';
$page .= '.php';
if( is_readable( $page ) ) require $page;
else require '404.php';
?>
</div>
<?php require 'bas_de_page.php' ?>
</body>
</html>

Avec ce genre de fonctionnement, les apprentis développeurs se concentrent sur le script de contenu avec la certitude de toujours avoir un menu et un bas de page.

Seulement, si on veut utiliser la fonction "header", mettre en place un cookie ou démarrer la session, vous comprendrez vite qu'il y a un problème. On a déjà sorti beaucoup de texte et on cherche à envoyer des informations d'en-tête.

Pourtant ce type de concept est une formidable opportunité pour séparer le code de traitement du code d'affichage (voir article Structurez vos applications "web"). Pourquoi ne pas utiliser le code suivant :

<?php
// Nom de la page
$page = isset( $_GET['p'] ) ? basename($_GET['p']) : 'defaut';
$page .= '.php';

//On en profite pour adapter le titre qui peut être
// modifié par le traitement
$titre = 'Mon super site';

// Y-a-t-il un traitement à effectuer ?
if( is_readable( 'traite/' . $page ) ) require 'traite/' . $page;
else require 'traite/defaut.php';
?>
<html>
<head>
<title><?php echo $titre ?></title>
<body>
<?php require 'frame/menu.php' ?>
<div id="contenu">
<?php
// on affiche le contenu
if( is_readable( 'frame/' . $page ) ) require 'frame/' . $page;
else require 'frame/404.php';
?>
</div>
<?php require 'frame/bas_de_page.php' ?>
</body>
</html>

Certes, il y a désormais deux scripts à éditer par page, mais vous avez une bonne séparation du traitement et de l'affichage. De plus, l'édition d'un script de traitement n'est pas obligatoire, et vous pouvez utiliser le script par défaut pour démarrer par exemple votre session.

En résumé, "headers already sent" indique obligatoirement que vous avez généré un contenu avant l'utilisation d'une fonction générant une information d'en-tête, que ce soit volontairement ou involontairement. De plus, il ne faut pas se dire je n'ai pas le choix, car il y a toujours une alternative, meilleure de surcroît.

Par Frédéric Bouchery
ADAM Benjamin 2008 | Admin