web.configにジェネリックハンドラを結びつけよう
次に「web.config」ファイルに設定を行います。どのWindowsのバージョンでも同一です。
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.web> <customErrors mode="On" defaultRedirect="/testredirect/RedirectorHandler.ashx"> <error statusCode="404" redirect="/testredirect/RedirectorHandler.ashx" /> <error statusCode="403" redirect="/testredirect/RedirectorHandler.ashx" /> </customErrors> </system.web> </configuration>
IIS 6までの場合には、アプリケーションのプロパティで[ASP.NET]タブの[構成の編集]ボタンから、[カスタムエラー]の[ローカルのカスタムエラー]でも設定可能です。
まず、IISマネージャでの設定は静的ファイルへの要求の場合に、ディレクトリ参照の要求で失敗した場合(403)や、ファイルの要求で失敗した場合(404)に処理されるURLを示しています。
「Web.config」への設定は、拡張子が.aspxや.asmxなど、ASP.NET関連エラーの場合です。
先ほど書いたような拡張子の要求の場合、IISがASP.NETに処理を渡してきます。ASP.NET側でどのような処理をするかはIIS側では感知しませんので、2か所に分けて記述するのです。
同様にASPClassicの設定が行われている場合には、.aspの拡張子の場合にASPClassic側に処理が渡ってしまうので、今回のものではフォローできません。このあと説明する処理と同様の処理をASP側で記述する必要があります。
URLを抜き出すコードを実装する
さて、いよいよ実装していきます。
IIS側で404エラーが発生した場合と、ASP.NET側で404エラーが発生した場合の2パターンで設定をしました。この2パターンにより、ジェネリックハンドラに渡されてくる情報も微妙に変化します。
今回の場合、リクエストされたURLでファイルが存在しない場合に、直近のディレクトリや直上のディレクトリへのリダイレクトを促す処理ですので、まずはリクエストされてきたURLを抜き出しましょう。
IIS側エラーの場合には、context.Request.Url.Queryに"?403;http://localhost:80/testredirect/"という情報が入っています。これはHTTPエラーの種類と、元アクセスURLが混ざった状態で入っています。
ASP.NET側エラーの場合にはcontext.Request.Url.Queryに"?aspxerrorpath=/testredirect/1.aspx"という情報が入っています。この場合は通常のリクエストと同じ記法で元URLが記述されていますので、context.Request["aspxerrorpath"]とするだけで、"/testredirect/1.aspx"が取得できることになります。
これらを正規表現などで抜き出しましょう。
private string もともと取得しようとしたパスを抽出(HttpContext context) { //もともと取得しようとしたURLを調べます。 if (context.Request["aspxerrorpath"] == null) { //IISからくる場合にはcontext.Request.Url.Queryに //元ページパスが格納されている string 対象URL = context.Request.Url.Query; Regex regex取得対象ページ抽出用 = new Regex(@"^\?\d+;http://\S+:\d+/(?<TARGET>[\s\S]+)"); MatchCollection 元取得ページ = regex取得対象ページ抽出用.Matches(対象URL); if (元取得ページ.Count > 0) { return 元取得ページ[0].Groups["TARGET"].Value; } else { return null; } } else { //ASPXからくる場合にはaspxerrorpathというパラメータでやってくる return context.Request["aspxerrorpath"]; } }
Private Function もともと取得しようとしたパスを抽出( _ ByVal context As HttpContext) As String 'もともと取得しようとしたURLを調べます。 If (context.Request("aspxerrorpath") Is Nothing) Then 'IISからくる場合にはcontext.Request.Url.Queryに '元ページパスが格納されている Dim 対象URL As String = context.Request.Url.Query Dim regex取得対象ページ抽出用 _ As New Regex("^\?\d+;http://\S+:\d+/(?<TARGET>[\s\S]+)") Dim 元取得ページ As MatchCollection _ = regex取得対象ページ抽出用.Matches(対象URL) If (元取得ページ.Count > 0) Then Return 元取得ページ(0).Groups("TARGET").Value Else Return Nothing End If Else 'ASPXからくる場合にはaspxerrorpathというパラメータでやってくる Return context.Request("aspxerrorpath") End If End Function
元リクエストパスを分解し、リダイレクト先URIを組み立てる
前述のURLを抜き出すと/testredirect/1.aspxのようなホスト名などがない完全なパス情報だけが残ります。この情報を/で分解し、一番後ろの情報だけ削除して次のリクエスト先を決定します。
//パスをリストに分解する List<string> 分割後対象パス = this.パスをリストに分解(元ページURL); //リダイレクト先URIを組みたてる string リダイレクト先URI = this.リダイレクト先URLを組みたてる(分割後対象パス); private List<string> パスをリストに分解(string 元ページURL) { //パスの分解を行う List<string> 分割後対象パス = new List<string>(元ページURL.Split('/')); //セキュリティのためと、IIS, ASPXの違いを吸収するためなどで // ".." と "" を排除する 分割後対象パス = 分割後対象パス.FindAll(delegate(string value) { if (value == null || value == "" || value == "..") { return false; } else { return true; } }); return 分割後対象パス; } private string リダイレクト先URLを組みたてる( List<string> 分割後対象パス) { //リダイレクト先URLを組みたてる StringBuilder sb = new StringBuilder(); 分割後対象パス.ForEach(delegate(string value) { sb.Append("/"); sb.Append(value); }); return sb.ToString(); }
'パスをリストに分解する Dim 分割後対象パス As List(Of String) _ = Me.パスをリストに分解(元ページURL) 'リダイレクト先URIを組みたてる Dim リダイレクト先URI As String _ = Me.リダイレクト先URLを組みたてる(分割後対象パス) Private Function リダイレクト先URLを組みたてる( _ ByVal 分割後対象パス As List(Of String)) As String 'リダイレクト先URLを組みたてる Dim sb As New StringBuilder() For Each value As String In 分割後対象パス sb.Append("/") sb.Append(value) Next Return sb.ToString() End Function Private Function パスをリストに分解(ByVal 元ページURL As String) _ As List(Of String) 'パスの分解を行う Dim 分割後対象パス As New List(Of String)(元ページURL.Split("/"c)) 'セキュリティのためと、IIS, ASPXの違いを吸収するためなどで ' ".." と "" を排除する 分割後対象パス _ = 分割後対象パス.FindAll(AddressOf Me.対象かどうかの判定) '要素がある場合には最後の要素を消してしまう If (分割後対象パス.Count > 0) Then 分割後対象パス(分割後対象パス.Count - 1) = "" End If Return 分割後対象パス End Function Private Function 対象かどうかの判定(ByVal value As String) As Boolean If (value Is Nothing OrElse value = "" OrElse value = "..") Then Return False Else Return True End If End Function
最後にはリダイレクトさせれば完了です。
context.Response.Redirect(リダイレクト先URI);
context.Response.Redirect(リダイレクト先URI)
最後に
静的なWebサイトの場合、URLは分かりやすい分類によって作成されている場合が多いと思います。手でURLを直接入力してサイトにアクセスしてくるユーザーもいますし、HTMLの記述ミスで404エラーを出してしまう場合もあります。
しかし、そのような場合においても、ユーザーによりよい情報を提供することで不快な思いをせずにすむならそれに越したことはありません。
今回紹介したコードの完全版が、このページの上の方にリンクされています。
是非あなたのサイトに組み込んで、ユーザビリティの向上の一助にしてください。

