Par Alexandre Alapetite le 2007-11-19 ; mise à jour 2008-04-30

Redirection HTTP gérée par une page d’erreur 404 personnalisée

La gestion des changements d’adresse des pages d’un site Web est quelque chose d’important dans la vie d’un site.

Pour cela, il existe principalement un système de redirection HTTP sous Apache, mais celui-ci présente plusieurs inconvénients comme le fait de n’être pas portable sous d’autres serveurs Web comme IIS, d’alourdir rapidement un site, et d’être difficilement testable ou portable sur un serveur avec une adresse différente. Dans le cas du serveur Microsoft IIS, la gestion des redirections est quasi inexistante.

En réponse à cela, je propose sur cette page un système de gestion des redirections basé sur les pages d’erreur 404 personnalisées. C’est-à-dire que le système de redirection n’est activé que lorsque qu’une ressource n’a pas été trouvée, ce qui supporte une montée en charge supérieure. Le système est compatible avec les serveurs Web Apache, Microsoft IIS, et d’autres. Une implémentation est déjà disponible en PHP, et le portage dans un autre langage est simple (Microsoft ASP.NET envisagé).

Télécharger le module

En construction

Sommaire

Quitter

Concept

Lorsqu’une page n’est pas trouvée, plutôt que d’afficher tout de suite une page d’erreur 404, un script va chercher dans une liste de règles si une redirection est disponible. Si une règles est disponible, une redirection HTTP sera envoyée au client ; sinon, une page d’erreur 404 sera affichée.

[Diagramme]

Avantages

Légèreté et montée en charge
Comme les règles de redirection ne sont lues que lorsqu’une ressource n’est pas trouvée, et que ces règles sont stoquées dans une arborescence permettant de réduire le nombre de règles à lire, le système et léger pour le serveur, et accepte une bonne montée en charge (nombre de requètes, et nombre de règles).
Portabilité et tests
Le système est compatible entre plusieurs types de serveurs différents (Apache, IIS, ...). De plus, comme des règles avec des adresses non-absolues peuvent être utilisées, cela permet de tester complètement les redirections sur un serveur de développement avant de les déployer sur le serveur de production.
Compatible avec les environnements mutualisés
La plupart des hébergements mutualisés (qu’ils soient sous Apache ou IIS) proposent une personnalisation des pages d’erreurs 404, et cela est la seule chose nécessaire pour faire marcher ce système.
Multi-langage
Le système de redirection proposé est facilement implémentable dans d’autres langages que PHP, comme ASP, ASP.NET, JSP ou autres CGI.

Structure

Arborescence

En admettant que vous placez le script de redirection dans /erreurs/redirection-404.php, celui-ci va par défaut chercher les règles de redirections dans /erreurs/404//404.txt, c’est à dire dans les fichiers 404.txt situés dans le dossier /erreurs/404/ ou ses sous-répertoires, comme l’illustre l’arborescence suivante :

Les noms des dossiers et sous-dossiers doivent correspondre à des dossiers du site actuel ou à des anciens dossiers à rediriger.

La profondeur et la largeur de l’arborescence ne sont pas limitées.

Faire des sous-répertoires n’est pas obligatoire et toutes les règles de redirections peuvent être dans /erreurs/404/404.txt. Mais cette possibilité d’arborescence permet d’éviter d’avoir un seul gros fichier 404.txt, permet de mieux structurer des règles ensemble, et permet d’augmenter les performances en évitant d’avoir trop de règles à lire dans un même fichier.
Un sous-répertoire est typiquement créé lorsqu’il y a plusieurs règles de redirection le concernant.

Dans le nom des dossiers, les caractères spéciaux non-acceptables dans une URL (en gros tout ce qui n’est pas a-z_.0-9-) doivent être %-encodés. En particulier, une espace sera encodée %20 (voir exemple ci-dessus : dossier%20avec%20espaces/).


Priorités

Lorsqu’un sous-dossier existe (ce qui n’est pas obligatoire), toutes les règles de redirection le concernant doivent se trouver dans son fichier 404.txt.
Au final, pour une requête de redirection donnée, un seul fichier 404.txt sera lu : celui le plus précis possible dans l’arborescence de /erreurs/404//.


Exemple

La syntaxe est expliquée juste après. Considérez déjà la règle de redirection suivante :

404.txt

permanent	/ancien-dossier/sous-dossier/fichier\.html	/nouveau-dossier/fichier.html

Cette règle pourra être placée dans le fichier /erreurs/404/404.txt, ou bien /erreurs/404/ancien-dossier/404.txt, ou encore /erreurs/404/ancien-dossier/sous-dossier/404.txt. Mais si le dossier /erreurs/404/ancien-dossier/ existe, cette règle ne sera pas lue si elle se trouve dans le dossier parent /erreurs/404/404.txt.


Personnalisation de la racine

Notez que par défaut, et contrairement aux fichiers .htaccess de Apache, les fichiers 404.txt ne sont pas mélangés au reste du site (ils sont dans /erreurs/404/), et cela pour éviter d’avoir à conserver d’anciens dossiers. Néanmoins, vous pouvez personnaliser l’endroit où les fichiers 404.txt sont cherchés à la ligne 27 du script redirection-404.php, dans la variable $path.


Syntaxe

La syntaxe utilisée pour les règles de redirection est proche de celle utilisée par l’instruction RedirectMatch de Apache ; voici les différences majeures :

  1. Les fichiers ou répertoires existants ne sont pas redirigés, car les règles de redirections ne sont lues que lorsqu’une ressource n’a pas été trouvée.
  2. Toutes les règles sont du style RedirectMatch (c’est-à-dire à base d’expressions régulières), et non simplement Redirect.
  3. Les règles doivent être écrites de manière extensive, c’est-à-dire qu’elles doivent décrire le nom complet du document à rediriger.
  4. L’ancienne adresse à rediriger peut être donnée relativement à la racine (/ancien-dossier/fichier\.html) comme Apache, ou relative en fonction de l’adresse du fichier 404.txt (comme par exemple fichier\.html si l’instruction se trouve dans /erreurs/404/ancien-dossier/404.txt ).
  5. Les adresses de redirection peuvent être absolues (http://example.net/), mais aussi relatives à la racine (/nouveau-dossier/fichier.html), ou relatives à l’ancien emplacement (nouveau-fichier.html)
  6. Les adresses redirigées ne sont pas sensibles à la casse (par défaut).

La syntaxe d’une règle de redirection est la suivante (une règle par ligne, et les trois parties de la règle sont séparées par des tabulations ou des espaces) :

404.txt

<type de redirection>	<ancienne adresse en expression régulière>	<nouvelle adresse>

Types de redirection

Les différents types de redirection pris en charge sont :

permanent
Pour une redirection permanente (HTTP/1.1 301 Moved Permanently)
temp ou found
Pour une redirection temporaire (HTTP/1.1 302 Found)
seeother
(Rare) Pour une redirection temporaire où le document (en particulier dynamique) a changé de type ou de fonctionnement (HTTP/1.1 303 See Other)
temporary
(Rare) Pour une redirection temporaire où le document (en particulier dynamique) a changé de type ou de fonctionnement (HTTP/1.1 307 Temporary Redirect)
gone
Pour indiquer qu’une ressource a été supprimée et n’a pas de redirection (HTTP/1.1 410 Gone)
Dans ce cas, il n’y a pas de troisième paramètre indiquant la nouvelle adresse.

Syntaxe des anciennes adresses à rediriger

En construction


Syntaxe des nouvelles adresses

En construction


Exemples

/erreurs/404/404.txt

#Redirige un fichier précis
permanent	/ancien-dossier/sous-dossier/fichier\.html	/nouveau-dossier/fichier.html
#Même exemple, avec un nom de répertoire et de fichier contenant une espace (codée %20)
permanent	/ancien%20dossier/mon%20fichier\.html	/nouveau%20dossier/mon%20fichier.html

#Redirige un site complet
permanent	/(.*)	http://nouveau-site.fr/$1
#Redirige uniquement la racine du site
permanent	/	/dossier/

#Redirige un répertoire et tous ses sous-dossiers et fichiers
permanent	/ancien-dossier/sous-dossier2/(.*)	/nouveau-dossier2/$1
#Même effet, en permettant aussi la redirection lorsque le / final est omis
permanent	/ancien-dossier/sous-dossier2(.*)	/nouveau-dossier2$1
#Redirige un répertoire mais pas ses sous-dossiers ni fichiers
permanent	/ancien-dossier/sous-dossier2/	/nouveau-dossier2/
#Redirige un répertoire et tous ses fichiers mais pas ses sous-dossiers
permanent	/ancien-dossier/sous-dossier2/([^/\\]*)	/nouveau-dossier2/$1

#Redirige en masse des fichiers correspondant à un motif donné,
#et réutilise une partie de leur nom (avec $n) pour former la nouvelle adresse
permanent	/images/image([0-9]+)\.(gif|jpg)	/images/image$1.png
#Même effet, syntaxe de destination allégée
permanent	/images/image([0-9]+)\.(gif|jpg)	image$1.png

#Redirection temporaire
#(l’adresse d’origine doit continuer à être celle utilisée et référencée)
temp	/raccourci	/adresse/plus-compliquee/

#Indique qu’une ressource n’est plus disponible
gone	/dossier/ancien\.pdf

Installation

Options

$path='./404/';
//Root of the redirection structure (404.txt files)
$customRedirect='/';
//To specify an optional custom HTML redirection after the error message
$customRedirectTimeOut=5;
//Time-out in second before redirection to the optional above address
$defaultNewServer='';
//Change the server of the new relative addresses: 'http://example.net:80'
$enableExternalServer=false;
//Allow this server to receive errors from another server

Serveur Apache

Sous Apache, l’ajout de cette page d’erreur personnalisée se fait via la directive ErrorDocument, qui peut en particulier être utilisée dans le fichier de configuration général ./conf/httpd.conf dans le répertoire d’installation de Apache, ou dans un fichier /.htaccess à la racine du site Web concerné, comme illustré ci-dessous :

/.htaccess

ErrorDocument 404 /erreurs/redirection-404.php

Logs Apache

Les redirections apparaissent dans les logs d’accès Apache, (codes 301, 302, 410…), et dans les logs d’erreur :

access.log

127.0.0.1 - - [05/Jan/2008:18:50:39 +0100] "GET /ancien-dossier/ HTTP/1.1" 301 651 "-" "Mozilla"
127.0.0.1 - - [05/Jan/2008:18:50:40 +0100] "GET /nouveau-dossier/ HTTP/1.1" 200 8397 "-" "Mozilla"
error.log

[Sat Jan 05 18:50:39 2008] [error] [client 127.0.0.1] File does not exist: E:/www/ancien-dossier

Déléguer la redirection à un autre serveur

(Facultatif et rare): Dans le cas où vous ne voulez ou ne pouvez pas traiter les redirection sur un serveur donné, l'option suivante permet de déléguer cette tâche à un autre serveur, après avoir activé cette possibilité dans la variable $enableExternalServer, ligne 31 de redirection-404.php.

/.htaccess

<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule ^(.*)$ http://example.net/erreurs/redirection-404.php?REDIRECT_URL=$1 [R=301,L]
</IfModule>

Serveur Microsoft IIS

Dans IIS, l’ajout de cette page d’erreur personnalisée se fait depuis sa console d’administration (accessible entre autres depuis les outils d’administration, "Gestion de l’ordinateur", ou "Gestionnaire des services Internet").

La configuration se fait ensuite dans les propriétés du site Web, dans l’onglet “Messages d’erreur personnalisés” (IIS 5 & 6) ou à partir de l’icone “Pages d’erreurs” (IIS 7). Après avoir sélectionné la ligne du code d’erreur 404, cliquer sur le bouton ou le lien “Modifier”, puis entrer une adresse de type URL indiquant le chemin du script redirection-404.php à partir de la racine du site Web comme par exemple /erreurs/redirection-404.php

[IIS erreur 404 personnalisées]

Dans IIS 7 (voir capture d’écran ci-dessus), le lien “Modifier les paramètres de fonction…” (à droite) permet entre autres de spécifier si l’on souhaite utiliser cette page d’erreur personnalisée y compris lorsqu’on se connecte en local (localhost), ce qui est désactivé par défaut.


Scripts de redirection HTTP pour page d’erreur 404 personnalisée

Implémentation en PHP

redirection-404.php

<?php
/*
 Script PHP pour gérer les redirections HTTP. À utiliser en tant que page d’erreur 404 personnalisée.
 http://alexandre.alapetite.net/doc-alex/redirection-404/
*/

//Constantes
$path=(empty($_SERVER['SCRIPT_FILENAME']) ? '' : dirname($_SERVER['SCRIPT_FILENAME'])).'/404/'; //Root of the redirection structure (404.txt files)
$customRedirect='/'; //To specify an optional custom HTML redirection after the error message
$customRedirectTimeOut=5; //Time-out in second before redirection to the optional above address
$defaultNewServer=''; //Change the server of the new relative addresses: 'http://example.net:80'
$enableExternalServer=false; //Allow this server to receive errors from another server

//Ancienne adresse
$oldUrl='';
if ($enableExternalServer&&isset($_GET['REDIRECT_URL'])) $oldUrl=substr($_GET['REDIRECT_URL'],0,1024); //Pour la redirection depuis un autre serveur
elseif (!empty($_SERVER['REQUEST_URI'])) $oldUrl=substr($_SERVER['REQUEST_URI'],0,1024); //Apache, IIS6
elseif (!empty($_SERVER['QUERY_STRING'])) $oldUrl=substr($_SERVER['QUERY_STRING'],0,1024); //IIS5
else $oldUrl='/_unknown_';
if (($sc=strpos($oldUrl,';'))!==false) $oldUrl=trim(substr($oldUrl,++$sc)); //IIS
$oldUrlParsed=parse_url($oldUrl);
$oldPath=$oldUrlParsed['path'];

//Cherche le meilleur fichier 404.txt
$absolute='/';
$dirs=explode('/',$oldPath); //Pas de urldecode(), donc les caractères spéciaux doivent être %-encodés : ./404/Bonjour%20Monde/404.txt
foreach($dirs as $dir)
 if (strlen($dir)>0)
 {
  if (is_dir($path.$dir))
  {
   $path.=$dir.'/';
   $absolute.=$dir.'/';
  }
  else break;
 }
$path.='404.txt';

//Cherche dans le fichier 404.txt la première correspondance pour $oldPath
$newPath='';
$httpStatus=302;
$found=false;
if (is_file($path)&&($handle=@fopen($path,'r')))
{
 while (!feof($handle))
 {
  $line=trim(fgets($handle,4096));
  if ((strlen($line)<3)||($line[0]=='#')) continue; //Commentaire ou invalide
  $map=preg_split('"\s+"',$line,4);
  if (count($map)<2) continue; //invalide
  $mapOld=$map[1];
  if ($mapOld[0]!='/') $mapOld=$absolute.$mapOld;
  if (@preg_match('"^'.$mapOld.'$"iD',$oldPath)&&
   ((($status=$map[0])==='gone')||
    ((count($map)>2)&&
     (strlen($newPath=@preg_replace('"^'.$mapOld.'$"iD',$map[2],$oldPath))>0))))
  {
   $found=true;
   switch ($status)
   {
    case 'permanent': $httpStatus=301; break;
    case 'found':
    case 'temp': $httpStatus=302; break;
    case 'seeother': $httpStatus=303; break;
    case 'temporary': $httpStatus=307; break;
    case 'gone': $httpStatus=410; break;
    default: $found=false;
   }
   if ($found&&(strlen($newPath)>0))
   {
    if (!preg_match('"^(?:(?:[a-z]{3,6}:)|(?:\.\./))"i',$newPath)) //Pas de protocole URI, et pas de ../ au début
    {//Quand c'est possible et que ce n'est pas déjà le cas, transforme en une URL absolue
     if (empty($defaultNewServer)&&isset($_SERVER['HTTP_HOST']))
     {
      $defaultNewServer='http://'.$_SERVER['HTTP_HOST'];
      if (isset($_SERVER['SERVER_PORT'])&&($_SERVER['SERVER_PORT']!='80'))
       $defaultNewServer.=':'.$_SERVER['SERVER_PORT'];
      if ($newPath[0]!=='/') //adresse relative
       $newPath=rtrim(substr($oldPath,-1,1)==='/' ? $oldPath : dirname($oldPath),'/\\').'/'.$newPath;
     }
     $newPath=$defaultNewServer.$newPath;
    }
    break;
   }
  }
 }
 fclose($handle);
}

if ($found) //Redirection si une nouvelle adresse a été trouvée
{
 if ($httpStatus===410)
 {
  header('HTTP/1.1 410 Gone');
  header('Status: 410 Gone');
  echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ',
   '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n",
   '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
   '<head>'."\n",
   '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'."\n",
   '<meta http-equiv="Content-Language" content="en-GB" />'."\n",
   empty($customRedirect) ? '' : '<meta http-equiv="Refresh" content="'.$customRedirectTimeOut.'; url='.$customRedirect.'" />'."\n",
   '<title>410 Gone</title>'."\n",
   '<meta name="language" content="en-GB" />'."\n",
   '<meta name="robots" content="noindex,follow" />'."\n",
   '</head>'."\n",
   '<body>'."\n",
   '<h1>Gone</h1>'."\n",
   '<p>The requested resource <kbd>'.$oldPath.'</kbd> is no longer available on this server and there is no forwarding address. ',
   'Please remove all references to this resource.</p>'."\n",
   '</body>'."\n",
   '</html>'."\n";
 }
 else
 {
  if (isset($oldUrlParsed['query'])) $newPath.='?'.$oldUrlParsed['query'];
  $status=array(301=>'Moved Permanently',302=>'Found',303=>'See Other',307=>'Temporary Redirect');
  header('Location: '.$newPath);
  header('HTTP/1.1 '.$httpStatus.' '.$status[$httpStatus]);
  header('Status: '.$httpStatus.' '.$status[$httpStatus]);
  echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ',
   '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n",
   '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
   '<head>'."\n",
   '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'."\n",
   '<meta http-equiv="Content-Language" content="en-GB" />'."\n",
   '<meta http-equiv="Refresh" content="0; url='.$newPath.'" />'."\n",
   '<title>'.$httpStatus.' '.$status[$httpStatus].'</title>'."\n",
   '<meta name="language" content="en-GB" />'."\n",
   '<meta name="robots" content="noindex,follow" />'."\n",
   '</head>'."\n",
   '<body>'."\n",
   '<h1>'.$status[$httpStatus].'</h1>'."\n",
   '<p>The document has moved <a href="'.$newPath.'">here</a>.</p>'."\n",
   '</body>'."\n",
   '</html>'."\n";
 }
}
else //Message d’erreur 404
{
 header('HTTP/1.1 404 Not Found');
 header('Status: 404 Not Found');
 echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ',
  '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n",
  '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
  '<head>'."\n",
  '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'."\n",
  '<meta http-equiv="Content-Language" content="en-GB" />'."\n",
  empty($customRedirect) ? '' : '<meta http-equiv="Refresh" content="'.$customRedirectTimeOut.'; url='.$customRedirect.'" />'."\n",
  '<title>404 Not Found</title>'."\n",
  '<meta name="language" content="en-GB" />'."\n",
  '<meta name="robots" content="noindex,follow" />'."\n",
  '</head>'."\n",
  '<body>'."\n",
  '<h1>Not Found</h1>'."\n",
  '<p>The requested <abbr title="Uniform Resource Locator">URL</abbr> <kbd>'.$oldPath.'</kbd> was not found on this server.</p>'."\n",
  '</body>'."\n",
  '</html>'."\n";
}
?>

Historique

1.1 2008-01-27
première distribution publique
1.0 2007-11-19
première version interne

Licence

Ce contenu est protégé par une licence Creative Commons Paternité - Partage des Conditions Initiales à l’Identique 2.0 France “BY-SA (FR)” [Creative Commons License]

Si vous utilisez et aimez ce logiciel (surtout pour un usage professionnel), merci de considérer faire un don.


Commentaires

Si vous souhaitez une réponse ou si c’est pour rapporter un problème avec ce script, merci de me contacter par courriel.

object : Voir les commentaires

http://alexandre.alapetite.net

Retour