by Alexandre Alapetite on 2007-11-19; updated 2015-01-06

HTTP redirection handled by personalised 404 error pages

In a Web site, the proper handling of address modifications (renaming, new structure, new technology etc.) is an important aspect of the life cycle.

To do so, there is for instance a broadly used HTTP redirection system under Apache, but it has some shortcomings such as not being portable to other Web servers such as IIS, with a risk to slow down a Web site, and to be difficult to test or migrate to a server with a different address or root (typical for a development server).

In the case of IIS, the handling of redirections can be fastidious, and not always available in the case of shared hosting.

In response to the problem, I propose on this page a redirection management system based on personalised 404 error pages. That is to say this redirection system is only activated when a resource is not found, thus supporting a higher load on the server. The system is compatible with Web servers such as Apache, Microsoft IIS, and others. An implementation is already available in PHP, and in ASP.NET. Finally, porting this system into another language is simple (JSP is planned).

Download PHP module

Download ASP.NET module

français

Table of contents

Exit

Concept

When a Web page is not found, instead of displaying a 404 error message right away, the script will search in a list of rules to see if a redirection is available. If such a rule is found, an HTTP redirection will be sent to the client; otherwise, a 404 error page will be displayed.

[Diagram]

Advantages

Light and high scalability:
As redirections rules are only read when a resource cannot be found, and since these rules are stored into a file tree reducing the number of rules to read, the system is light and fast for the server, and has a high scalability (number of request, and number of rules).
Portability and testing:
The system is compatible with several types of Web servers (Apache, IIS, …).
Furthermore, since relative addresses can be used, this allows testing redirections on a development server before deploying them on the production server.
In particular, this allows keeping the same rules, even if a Web site is relocated from the root of a server to a sub-folder such as http://example.net/~myWebSite/, or vice versa.
Compatible with shared Web hosting:
Many shared Web hosting service (should they be under Apache or IIS) offer a way to customise 404 error pages, and this is the only thing required for this system to work.
Multi-language:
The proposed redirection system is already available for PHP and ASP.NET. It is furthermore easily portable to other languages such as ASP classic, JSP or other CGI.
Index

Structure

File tree

Admitting that you will place the redirection script in a sub-folder /errors/ of your Web site (recommended), it will search by default for some redirections rules in /errors/404//404.txt, that is to say 404.txt files located in the folder /errors/404/ or any sub-folder, as illustrated in the following file tree:

The name of the folders and sub-folders must match existing or former folders of the Web site to redirect.

There is no limit on the deepness and breath of the tree structure.

Making use of sub-folders is not required and all redirection rules can be in the same file /errors/404/404.txt. However, this possibility of tree structure is interesting to avoid having one single large 404.txt file, to better manage related rules, and to improve the performances by avoiding the need to read many rules.
A sub-folder is typically created when there is a number of rules targeting it or its child.

In the name of the folders, special characters that are not acceptable in a URL (more or less everything except a-z_.0-9-) have to be %-encoded. In particular, a space will be encoded %20 (see above example: folder%20with%20spaces/).

Warning: the redirection script is set up by default to be in a sub-folder (level 1) of the Web site, such as /errors/ like in the example above. This can be modified in the constant distanceToRoot at the beginning of the script.


Priorities

When a sub-folder does exist (which is not mandatory), all the redirections rules targeting it must be stored into its 404.txt file.
In the end, for a given redirection request, only one single 404.txt file will be read: the most precise one in the tree structure of /errors/404//.


Example

The syntax is explained just after. Have a look already to the following redirection rule:

404.txt

permanent	/old-folder/sub-folder/file\.html	/new-map/file.html

This rule can be placed in the file /errors/404/404.txt, or /errors/404/old-folder/404.txt, or even /errors/404/old-folder/sub-folder/404.txt. However, if the folder /errors/404/old-folder/ does exist, the rule will not be read if it is stored into the parent folder /errors/404/404.txt.


Customizing the folder containing the redirection rules

Note that by default, as opposed to Apache’s .htaccess files, the 404.txt files are not mixed with the rest of the site (they are in /errors/404/), and this is in order to avoid having to maintain old folder (or to have one large file at the root). Nevertheless, it is possible to customize the location where these 404.txt files are searched, thanks to the path404 variable at the beginning of the script.

Index

Syntax

The syntax of a redirection rule is the following (one rule per line, and the three parts of the rules are separated by tabulations or spaces):

404.txt

<redirection type>	<regular expression of the old address>	<new address>

Differences with Apache

The syntax used for the redirection rules is similar to the one used by the Apache’s instruction RedirectMatch; here are the main differences:

  1. The existing files or directories are not redirected, because the redirections rules are only read when a resource was not found.
  2. All the rules are of the RedirectMatch style (i.e. based on regular expressions), and not simply Redirect.
  3. The rules must be written in an extensive manner, that is to say describing the name of the resource to redirect from beginning to end.
  4. The redirected addresses are not case-sensitive (by default).
  5. If a Web site is not at the root of its Web site, like http://example.net/~myWebSite/, the redirections can be expressed in a more convenient manner from the root of the Web site instead of the root of the server.
  6. The former address can be given as a relative address, depending on the location of the 404.txt file (such as abbreviating /old-folder/file\.html as file\.html if the instruction is located in /errors/404/old-folder/404.txt).
  7. The redirection addresses (new ones) can be given relatively to the root of the site (/new-folder/file.html), or even relatively to the former location (new-file.html), instead of requiring a full URL as Apache does, like http://example.net/new-folder/file.html.

Redirection types

The different types of redirections are:

permanent
For a permanent redirection (HTTP/1.1 301 Moved Permanently)
temp or found
For a temporary redirection (HTTP/1.1 302 Found)
seeother
(Rare) For a temporary redirection where the document (in particular if dynamic) has changed type or functionality (HTTP/1.1 303 See Other)
temporary
(Rare) For a temporary redirection where the document (in particular if dynamic) has changed type or functionality (HTTP/1.1 307 Temporary Redirect)
gone
To indicate that a resource has been permanently removed without redirection (HTTP/1.1 410 Gone)
In that case, there is no third part in the rule, to indicate a new address.

Syntax of the former addresses to redirect

The old addresses are written as regular expressions. If you are not familiar with this syntax, just write the old address by taking care to treat some special characters are follow:

The old addresses are understood from the root of the Web site; that is to say they neither contain http:// nor the name of the server.
For instance, to redirect http://example.net/old-folder/, the old address will be written /old-folder/ simply.

In particular, if a Web site is not at the root of is Web server, like http://example.net/~myWebSite/, the redirections are understood from the root of the Web site and not from the root of the server (this can be set up in the distanceToRoot constant at the beginning of the script).
For instance, to redirect http://example.net/~myWebSite/old-folder/, the old address will be written /old-folder/ only.

The former address can optionally be abbreviated according to the location of the 404.txt file (such as abbreviating /old-folder/file\.html as file\.html if the instruction is located in /errors/404/old-folder/404.txt). However, the full syntax given relatively to the root is recommended for its flexibility and robustness.


Syntax of the new addresses

The new addresses are not regular expressions; anti-slash escaping must therefore not be used.

It is possible to use $1, $2 etc. to reference a captured () coming from the regular expression of the former address.

The redirection addresses can be absolute (http://example.net/new-folder/filer.html), or also relative to the root of the site (/new-folder/file.html) [recommended], or relative to the former location (new-file.html)

Index

Examples

/errors/404/404.txt

#Redirect a precise file
permanent	/old-folder/sub-folder/file\.html	/new-folder/file.html
#Same example, but with a folder name containing a space (coded %20)
permanent	/old%20folder/my%20file\.html	/new%20folder/my%20file.html

#Redirect a full Web site
permanent	/(.*)	http://new-site.net/$1
#Redirect only the root of the site
permanent	/	/folder/

#Redirect a folder and its sub-folders and files
permanent	/old-folder/sub-folder2/(.*)	/new-folder2/$1
#Same effect, allowing also the redirection when the trailing / is omitted
permanent	/old-folder/sub-folder2(.*)	/new-folder2$1
#Redirect a folder but neither its sub-folder nor files
permanent	/old-folder/sub-folder2/	/new-folder2/
#Redirect a folder and its files but not its sub-folders
permanent	/old-folder/sub-folder2/([^/\\]*)	/new-folder2/$1

#Mass redirection of all the files with a given pattern,
#and reuse a part of their name (with $n) to make the new address
permanent	/images/image([0-9]+)\.(gif|jpg)	/images/image$1.png
#Same effect, with a abbreviated destination address
permanent	/images/image([0-9]+)\.(gif|jpg)	image$1.png

#Temporary redirection
#(the original address must still be kept as the one used and referenced)
temp	/shortcut	/longer-address/more-complicated/

#Indicates that a resource is no longer available
gone	/folder/old\.pdf
Index

Installation

Options

A number of options can be customised via the constants at the beginning of the script:

distanceToRoot = 1
Number of levels from the root of the Web site, to this script, for automatic handling of Web sites which root is in a sub-folder of the Web server.
Set to -1 to disable this functionality.
For example, set to 0 if this script is in http://example.com/ or http://example.net/~myWebSite/
For example, set to 1 if this script is in http://example.com/errors/ or http://example.net/~myWebSite/errors/
For example, set to 2 if this script is in http://example.com/folder/errors/ or http://example.net/~myWebSite/folder/errors/
path = "./404/"
Root of the folder containing the redirection (404.txt files)
customRedirect = "/"
To specify an optional custom HTML redirection after the error message
The default value "/" makes a redirection to the root of the Web site (that can be different from the root of the server).
customRedirectTimeOut = 5
Delay in second before redirection to the optional above address
defaultNewServer = ""
Change the server for redirections using relative addresses: for example "http://example.net:80"
allowASPmode = true
Allow old addresses to be given as a parameter (ASP.NET mode, or to receive 404 errors from an external server)

Apache server

Under Apache, this personalised error page is added via the ErrorDocument directive, which can in particular be used in the general configuration file ./conf/httpd.conf located in Apache’s installation directory, or in a /.htaccess file at the root of the Web site of interest, as exemplified below:

/.htaccess

ErrorDocument 404 /errors/redirection-404.php

Apache log files

The redirections appear in Apache’s access logs, (codes 301, 302, 410…), as well as in the error logs:

access.log

127.0.0.1 - - [05/Jan/2008:18:50:39 +0100] "GET /old-folder/ HTTP/1.1" 301 651 "-" "Mozilla"
127.0.0.1 - - [05/Jan/2008:18:50:40 +0100] "GET /new-folder/ 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/old-folder

Delegating the redirection to another server

(Optional and rare): In the case when you do not want or cannot treat the redirections on a given server, the following method gives a possibility to delegates this task to another server, after having enabled this feature in the allowASPmode variable, at the beginning of the redirection script.

/.htaccess

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

Microsoft IIS server

In IIS, this personalised error page is added from the administration panel.

The setup is then done in the properties of the Web site, in the tab “Custom errors” (IIS 5 & 6) or from the icon “Error pages” (IIS 7). After having selected the line for the 404 error code, click on the button or the link “Edit”, and then enter an address of the type URL indicating the path for the script redirection-404 relatively to the root of the Web site like for instance /errors/redirection-404.aspx or /errors/redirection-404.php.

[IIS custom 404 errors]

In IIS 7, the link “Edit Feature Settings…” (on the right) allows among other things to specify if this personalised error page should be served also for local connections (localhost), which is disabled by default.


Manual setup of IIS

In IIS 6 and newer, the above setup in graphic mode can also be done manually, with the directive httpErrors as follow:

/Web.config

<configuration>
	<system.webServer>
		<httpErrors errorMode="Custom"><!-- For non-managed files -->
			<remove statusCode="404" subStatusCode="-1" />
			<error statusCode="404" path="/errors/redirection-404.aspx" responseMode="ExecuteURL" />
		</httpErrors>
	</system.webServer>
	…
</configuration>

Note that in order to be allowed to put this instruction at the local level in a Web.config at the root of your site, instead of in the glogal applicationHost.config, it is required to enable the following line in applicationHost.config:

%windir%\System32\inetsrv\config\applicationHost.config

	<section name="httpErrors" overrideModeDefault="Allow" />

Special setup for ASP.NET files

Redirecting .aspx files requires an additional configuration.

ASP.NET under IIS 6 and older

Until IIS 6, there was a possibility to check for the existence of .aspx files before sending them to the ASP.NET engine, in [Properties / Home Directory / Configuration… / Mappings / .aspx / Edit] (screenshot of IIS 5):

[IIS check file exists]
ASP.NET under IIS 7.5 and newer

If IIS 7.5+ is used, or if the above option cannot be used, then an additional instruction customErrors must be used for ASP.NET files such as .aspx. Furthermore, the redirection script must have the allowASPmode variable enabled at the beginning of the redirection script.

/Web.config

<configuration>
	…
	<system.web>
		<customErrors mode="On" redirectMode="ResponseRewrite">
			<error statusCode="404" redirect="/errors/redirection-404.aspx" />
		</customErrors>
	</system.web>
</configuration>

Note that the attribute redirectMode with the value ResponseRewrite removes the problematic intermediary 302 redirection that occurs by default, but this is only available from ASP.NET 3.5 SP1 and IIS 7.5 (Windows 7).

Universal setup for ASP.NET with a module

If you cannot use the above methods to ensure the redirection of .aspx files, there is still the possibility to write a custom module to handle errors, which has the advantage of working with IIS 6 and 7.0+:

/App_Code/Redirection404Helper.cs

using System;
using System.Web;

/// <summary>Error 404 handler helper. 2008-10-20</summary>
/// <see cref="https://alexandre.alapetite.fr/doc-alex/redirection-404/"/>
public class Redirection404Helper : IHttpModule
{
	public void Dispose() {}

	public void Init(HttpApplication context)
	{
		context.Error += new EventHandler(On404Error);
	}

	void On404Error(object sender, EventArgs e)
	{
		HttpApplication httpApplication = sender as HttpApplication;
		if (httpApplication == null) return;
		HttpContext httpContext = httpApplication.Context;
		HttpException httpException = httpContext.Error as HttpException;
		if (httpException == null) return;
		if (httpException.GetHttpCode() == 404)
			httpContext.Server.Transfer(httpContext.Request.ApplicationPath +
				@"/errors/Redirection-404.aspx?aspxerrorpath=" + httpContext.Request.RawUrl);
	}
}

Place the C# file above in the folder /App_Code/ of your Web site (paying attention to the address of the redirection script on the last line of the module), then reference it as follow (can also be setup graphically via the “Modules” icon, then the link “Add Managed Module…”):

/Web.config

<configuration>
	<system.webServer>
		<httpErrors errorMode="Custom"><!-- For non-managed files -->
			<remove statusCode="404" subStatusCode="-1" />
			<error statusCode="404" path="/errors/redirection-404.aspx" responseMode="ExecuteURL" />
		</httpErrors>
		<!-- To have both section for IIS 6 and 7+ at the same time -->
		<validation validateIntegratedModeConfiguration="false" />
		<modules><!-- For IIS 7+ -->
			<add name="Redirection404Helper" type="Redirection404Helper" preCondition="managedHandler" />
		</modules>
	</system.webServer>
	<system.web>
		<httpModules><!-- For IIS 6 -->
			<add name="Redirection404Helper" type="Redirection404Helper" />
		</httpModules>
	</system.web>
</configuration>
Index

Apache Tomcat server

Under construction…

./conf/web.xml

<error-page>
	<error-code>404</error-code>
	<location>/errors/redirection-404.jsp</location>
</error-page>
Index

HTTP redirection scripts for custom 404 error pages

Implementation in PHP

redirection-404.php

<?php
/*
 PHP script handling HTTP redirections. To be used as a custom HTTP 404 error page.
 https://alexandre.alapetite.fr/doc-alex/redirection-404/
*/

//--<Constants>--

//Number of levels from the root of the Web site, to this script, for automatic handling of Web sites which root is in a sub-folder of the Web server.
//Set to -1 to disable this functionality.
//For example, set to 1 if this script in http://example.com/errors/ or http://example.net/~myWebSite/errors/
$distanceToRoot=1;

//Root of the folder containing the redirection (404.txt files)
$path404=(empty($_SERVER['SCRIPT_FILENAME']) ? '404/' : dirname($_SERVER['SCRIPT_FILENAME'])).'/404/';

$customRedirect='/';	//To specify an optional custom HTML redirection after the error message
$customRedirectTimeOut=5;	//Delay in second before redirection to the optional above address

$defaultNewServer='';	//Change the server for redirections using relative addresses: for example 'http://example.net:80'

$allowASPmode=true;	//Allow old addresses to be given as a parameter (ASP.NET mode, or to receive 404 errors from an external server)

//--</Constants>--

//Get old address
$oldUrl='';
if (!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 (defined but empty under Apache)
else $oldUrl='/_unknown_';
if (($sc=strpos($oldUrl,';'))!==false) $oldUrl=trim(substr($oldUrl,++$sc));	//IIS
$oldUrlParsed=strpos($oldUrl,'://')===false ? @parse_url($oldUrl) : '';
if (empty($oldUrlParsed))
{
 $oldUrl='/_unknown_';
 $oldUrlParsed=parse_url($oldUrl);
}
if ($allowASPmode&&(!empty($oldUrlParsed['query'])))	//Special ASP.NET
{
 parse_str($oldUrlParsed['query'],$oldquery);
 if (!empty($oldquery['aspxerrorpath'])) $oldUrlParsed=parse_url($oldquery['aspxerrorpath']);
}
$oldPath=$oldUrlParsed['path'];
$siteRoot='';	//For the case when the Web site is not at the root of the Web server, such as http://example.net/~myWebSite/
if (($distanceToRoot>=0)&&(!empty($_SERVER['SCRIPT_NAME'])))
{
 $map404=$_SERVER['SCRIPT_NAME'];
 if (substr($map404,-1)!=='/') $map404=dirname($map404);
 $map404=trim($map404,'/\\');
 $dirs=explode('/',$map404);
 $nbSubLevels=count($dirs)-$distanceToRoot;
 for ($i=0;$i<$nbSubLevels;$i++) $siteRoot.='/'.$dirs[$i];
 if (!empty($siteRoot))
 {
  if (strcasecmp(substr($oldPath,0,strlen($siteRoot)),$siteRoot)===0) $oldPath=substr($oldPath,strlen($siteRoot));
  if ((!empty($customRedirect))&&($customRedirect[0]==='/')) $customRedirect=$siteRoot.$customRedirect;
 }
}

//Search the best 404.txt mapping file in the file-tree structure
$absolute='/';
$dirs=explode('/',$oldPath);	//We do not do urldecode(), so special characters (e.g. space) of local folders must be %-encoded: ./404/Hello%20World/404.txt
foreach ($dirs as $dir)
 if (strlen($dir)>0)
 {
  if (($dir[0]!=='.')&&is_dir($path404.$dir))
  {
   $path404.=$dir.'/';
   $absolute.=$dir.'/';
  }
  else break;
 }
$path404.='404.txt';

//Search in the 404.txt file for the first matching for $oldPath
$newPath='';
$httpStatus=302;
$found=false;
if (is_file($path404)&&($handle=@fopen($path404,'r')))
{
 while (!feof($handle))
 {
  $line=trim(fgets($handle,4096));
  if ((strlen($line)<3)||($line[0]=='#')) continue;	//comment or invalid
  $map=preg_split('"\s+"',$line,4);
  if (count($map)<2) continue;	//invalid
  $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))))
  {
   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: continue 2;
   }
   $found=true;
   if ($httpStatus!==410)
   {
    if (!empty($siteRoot)) $newPath=$siteRoot.$newPath;
    if (!preg_match('"^(?:(?:[a-z]{3,6}:)|(?:\.\./))"i',$newPath))	//No URI Scheme, and no ../ in front
    {//When it is possible and not already the case, make the redirection an absolute URL
     if (empty($defaultNewServer)&&isset($_SERVER['HTTP_HOST']))
     {
      if (!empty($_SERVER['SERVER_PORT']))
      {
       if (empty($_SERVER['HTTPS']))
       {
        if ($_SERVER['SERVER_PORT']!='80') $defaultNewServer.=':'.$_SERVER['SERVER_PORT'];
       }
       elseif ($_SERVER['SERVER_PORT']!='443') $defaultNewServer.=':'.$_SERVER['SERVER_PORT'];
      }
      if (empty($newPath)||($newPath[0]!=='/'))	//relative address
       $newPath=rtrim(substr($oldPath,-1)==='/' ? $oldPath : dirname($oldPath),'/\\').'/'.$newPath;
     }
     $newPath=$defaultNewServer.$newPath;
    }
   }
   break;
  }
 }
 fclose($handle);
}

if ($found)	//Redirect if new address is found
{
 if ($httpStatus===410)
 {
  header('HTTP/1.1 410 Gone');
  header('Status: 410 Gone');
  echo '<!DOCTYPE html>'."\n",
   '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
   '<head>'."\n",
   '<meta charset="UTF-8" />'."\n",
   empty($customRedirect) ? '' : '<meta http-equiv="Refresh" content="'.$customRedirectTimeOut.'; url='.$customRedirect.'" />'."\n",
   '<title>410 Gone</title>'."\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>'."\n",
   '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
   '<head>'."\n",
   '<meta charset="UTF-8" />'."\n",
   '<meta http-equiv="Refresh" content="0; url='.$newPath.'" />'."\n",
   '<title>'.$httpStatus.' '.$status[$httpStatus].'</title>'."\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	//404 error message
{
 header('HTTP/1.1 404 Not Found');
 header('Status: 404 Not Found');
 echo '<!DOCTYPE html>'."\n",
  '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
  '<head>'."\n",
  '<meta charset="UTF-8" />'."\n",
  empty($customRedirect) ? '' : '<meta http-equiv="Refresh" content="'.$customRedirectTimeOut.'; url='.$customRedirect.'" />'."\n",
  '<title>404 Not Found</title>'."\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";
}
?>

History

1.10 2020-08-09
Corrected a bug with continue in PHP7.3+
1.9 2015-01-06
Corrected a bug with Gone
1.8 2011-09-05
Better support for HTTPS and custom ports
HTML5
Removed a warning for when parsing some invalid addresses
1.7 2010-01-31
Corrected a bug with Gone
1.6 2009-06-22
Error handling when parsing requested addresses
1.5 2008-10-19
Case-insensitive for some detections
1.4 2008-10-04
Added redirections of ASP.NET files
1.3 2008-09-29
Added automatic handling of Web sites that are not at the root of their Web server
1.1 2008-01-27
First public distribution
1.0 2007-11-19
First internal version
Index

Implementation in ASP.NET

Redirection-404.aspx

<%--
 ASP.NET script handling HTTP redirections. To be used as a custom HTTP 404 error page.
 https://alexandre.alapetite.fr/doc-alex/redirection-404/
--%>
<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>
<script runat="server">
 protected void Page_Load(object sender, EventArgs e)
 {
  #region --Constants--

 	//Number of levels from the root of the Web site, to this script, for automatic handling of Web sites which root is in a sub-folder of the Web server.
  //Set to -1 to disable this functionality.
  //For example, set to 1 if this script in http://example.com/errors/ or http://example.net/~myWebSite/errors/
  int distanceToRoot = 1;

 	//Root of the redirection structure (404.txt files)
  string path = Path.GetDirectoryName(Server.MapPath(Request.CurrentExecutionFilePath)) + @"/404/";
  
  string customRedirect = @"/";	//To specify an optional custom HTML redirection after the error message
  int customRedirectTimeOut = 5;	//Time-out in second before redirection to the optional above address
  
  string defaultNewServer = @"";	//Change the server of the new relative addresses: 'http://example.net:80'
  
  bool allowASPmode = true;	//Allow old addresses to be given as a parameter (ASP.NET mode, or to receive 404 errors from an external server)
  
  #endregion

  #region Get old address
  string oldUrl = "http://localhost" + Request.RawUrl;
  int sc = oldUrl.IndexOf("404;");
  if (sc >= 0) oldUrl = oldUrl.Substring(sc + 4).Trim();	//IIS
  Uri oldUrlParsed = null;
  try
  {
   oldUrlParsed = new Uri(oldUrl);
   if (allowASPmode && (!String.IsNullOrEmpty(oldUrlParsed.Query)))	//Special ASP.NET
   {
    NameValueCollection oldquery = HttpUtility.ParseQueryString(oldUrlParsed.Query);
    if (!String.IsNullOrEmpty(oldquery["aspxerrorpath"])) oldUrlParsed = new Uri("http://localhost" + oldquery["aspxerrorpath"]);
   }
  }
  catch
  {
   oldUrlParsed = new Uri(@"http://localhost/_unknown_");
  }
  string oldPath = oldUrlParsed.AbsolutePath;
  string siteRoot = "";	//For the case when the Web site is not at the root of the Web server, such as http://example.net/~myWebSite/
  string[] dirs;
  if (distanceToRoot >= 0)
  {
   string map404 = Request.CurrentExecutionFilePath;
   if (map404[map404.Length - 1] != '/') map404 = Path.GetDirectoryName(map404);
   map404 = map404.Trim(new char[] { '/', '\\' }).Replace('\\', '/');
   dirs = map404.Split('/');
   int nbSubLevels = dirs.Length - distanceToRoot;
   for (int i = 0; i < nbSubLevels; i++) siteRoot += '/' + dirs[i];
   if (!String.IsNullOrEmpty(siteRoot))
   {
    if (oldPath.StartsWith(siteRoot, StringComparison.InvariantCultureIgnoreCase)) oldPath = oldPath.Substring(siteRoot.Length);
    if ((!String.IsNullOrEmpty(customRedirect)) && (customRedirect[0] == '/')) customRedirect = siteRoot + customRedirect;
   }
  }
  #endregion

  #region Search the best 404.txt mapping file in the file-tree structure
  string absolute = "/";
  dirs = oldPath.Split('/');	//We do not do Server.UrlDecode(), so special characters (e.g. space) of local folders must be %-encoded: ./404/Hello%20World/404.txt
  foreach (string dir in dirs)
   if (dir.Length > 0)
   {
    if ((dir[0] != '.') && Directory.Exists(path + dir))
    {
     path += dir + '/';
     absolute += dir + '/';
    }
    else break;
   }
  path += "404.txt";
  #endregion

  #region Search in the 404.txt file for the first matching for oldPath
  string newPath = "";
  int httpStatus = 302;
  bool found = false;
  FileInfo fileInfo = new FileInfo(path);
  StreamReader streamReader = null;
  try
  {
   if (fileInfo.Exists && ((streamReader = fileInfo.OpenText()) != null))
   {
    string line;
    while ((line = streamReader.ReadLine()) != null)
    {
     if (line.Length > 4096) line = line.Substring(0, 4096);
     line = line.Trim();
     if ((line.Length < 3) || (line[0] == '#')) continue;	//comment or invalid
     string[] map = Regex.Split(line, @"\s+");
     if (map.Length < 2) continue;	//invalid
     string status = map[0];
     string mapOld = map[1];
     if (mapOld[0] != '/') mapOld = absolute + mapOld;
     if (Regex.Match(oldPath, "^" + mapOld + '$', RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Success &&
      ((status == "gone") ||
       ((map.Length > 2) && ((newPath = Regex.Replace(oldPath, "^" + mapOld + '$', map[2], RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).Length > 0))
      ))
     {
      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: continue;
      }
      found = true;
      if (found && (httpStatus != 410))
      {
       if (!String.IsNullOrEmpty(siteRoot)) newPath = siteRoot + newPath;
       if (!Regex.Match(newPath, @"^(?:(?:[a-z]{3,6}:)|(?:\.\./))", RegexOptions.IgnoreCase).Success)	//No URI Scheme, and no ../ in front
       {//When it is possible and not already the case, make the redirection an absolute URL
        if (defaultNewServer.Length < 8)
        {
         defaultNewServer = "http://" + Request.ServerVariables["HTTP_HOST"];
         if (String.IsNullOrEmpty(newPath) || (newPath[0] != '/'))	//relative address
          newPath = (oldPath[oldPath.Length - 1] == '/' ? oldPath : Path.GetDirectoryName(oldPath).Replace('\\', '/')).TrimEnd(new char[] { '/', '\\' }) + '/' + newPath;
        }
        newPath = defaultNewServer + newPath;
       }
      }
      break;  
     }
    }
   }
  }
  catch (Exception ex)
  {
   Trace.Write("Redirection-404", "Error while reading a 404.txt file", ex);
  }
  finally
  {
   if (streamReader != null) streamReader.Close();
   fileInfo = null;
  }
  #endregion

  #region Response
  if (found)	//Redirect if new address is found
  {
   if (httpStatus == 410)
   {
    Response.Status = "410 Gone";
    Response.Write(@"<!DOCTYPE html>
<html xmlns=""http://www.w3.org/1999/xhtml"" xml:lang=""en-GB"" lang=""en-GB"">
<head>
<meta charset=""UTF-8"" />
");
    if (customRedirect.Length > 0)
     Response.Write(string.Format("<meta http-equiv=\"Refresh\" content=\"{0}; url={1}\" />\n", customRedirectTimeOut, customRedirect));
    Response.Write(@"<title>410 Gone</title>
<meta name=""robots"" content=""noindex,follow"" />
</head>
<body>
<h1>Gone</h1>
<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>
</body>
</html>
");
   }
   else
   {
    if (!String.IsNullOrEmpty(oldUrlParsed.Query)) newPath += oldUrlParsed.Query;
    Response.RedirectLocation = newPath;
    switch (httpStatus)
    {
     case 301: Response.Status = "301 Moved Permanently"; break;
     case 302: Response.Status = "302 Found"; break;
     case 303: Response.Status = "303 See Other"; break;
     case 307: Response.Status = "307 Temporary Redirect"; break;
     default: Response.Status = "302 Found"; break;
    }
    Response.Write(@"<!DOCTYPE html>
<html xmlns=""http://www.w3.org/1999/xhtml"" xml:lang=""en-GB"" lang=""en-GB"">
<head>
<meta charset=""UTF-8"" />
<meta http-equiv=""Refresh"" content=""0; url=" + newPath + @""" />
<title>" + Response.Status + @"</title>
<meta name=""robots"" content=""noindex,follow"" />
</head>
<body>
<h1>" + Response.StatusDescription + @"</h1>
<p>The document has moved <a href=""" + newPath + @""">here</a>.</p>
</body>
</html>
");
   }
  }
  else	//404 error message
  {
   Response.Status = "404 Not Found";
   Response.Write(@"<!DOCTYPE html>
<html xmlns=""http://www.w3.org/1999/xhtml"" xml:lang=""en-GB"" lang=""en-GB"">
<head>
<meta charset=""UTF-8"" />
");
   if (customRedirect.Length > 0)
    Response.Write(string.Format("<meta http-equiv=\"Refresh\" content=\"{0}; url={1}\" />\n", customRedirectTimeOut, customRedirect));
   Response.Write(@"<title>404 Not Found</title>
<meta name=""robots"" content=""noindex,follow"" />
</head>
<body>
<h1>Not Found</h1>
<p>The requested <abbr title=""Uniform Resource Locator"">URL</abbr> <kbd>" + oldPath + @"</kbd> was not found on this server.</p>
</body>
</html>
");
  }
  #endregion
 }
</script>

History

1.9 2015-01-06
Corrected a bug with Gone
1.8 2011-09-05
Better support for HTTPS and custom ports
HTML5
1.7 2010-01-31
Corrected a bug with Gone
1.5 2008-10-19
Possibility to be used with some URL-rewriting
Case-insensitive for some detections
1.4 2008-10-04
Added redirections of ASP.NET files
1.3 2008-09-29
First public distribution
Index

Licence

This content is protected by a licence Creative Commons Attribution-ShareAlike 2.0 France "BY-SA (FR)" [Creative Commons License]

If you use and like this library (especially for professional purposes), please consider doing a donation.


Comments

If you expect an answer or if it is to report a problem, please contact me by e-mail.

object: View comments

https://alexandre.alapetite.fr

Back