はじめに
どんなアプリケーションにもエラー処理が必要です。そのことは誰でも知っています。しかし、我々開発者は、処理されないエラーがクライアントマシン上で発生した場合に、それを知ることができません。Webの良いところは、処理されないエラーが発生したときに、それを必ず知ることができるという点です。ASP.NETの登場により、エラーを処理するための優れた方法が新しく利用できるようになりました。.NETでは、エラーの処理方法や情報の提供方法に関していくつか違いがあります。たとえば、従来のASPではASPError
オブジェクトを返すときにServer.GetLastError
メソッドを使用していました。.NETでもServer.GetLastError
を使用することはできますし、実際にそうすべきですが、.NETではこのメソッドがSystem.Exception
型を返すようになっています。.NET内ではあらゆるものを一貫させようとするMicrosoftの姿勢は評価できますし、これは歓迎すべき仕様変更です。
問題
アプリケーションではエラーが起こります。我々開発者はtry-catch
ブロック(従来のASPではon error resume next
のみ可能)を使用して大部分のエラーをトラップしようとしますが、考えられるすべての例外をカバーできるわけではありません。処理されないエラーが発生したらどうなるでしょうか? 通常は、IISの既定のエラーページがユーザーに表示されます(このページは通常は「c:\winnt\help\iishelp\common」にあります)。問題は、このようなエラーが発生しても我々は関知できず、サイトのルックアンドフィールとは関係のないエラーページが表示されてしまうということです。エラーは開発につきものですが、我々はできるだけエラーを排除し、きちんと処理するよう努めなければなりません。エラー処理のためには、次のことを知る必要があります。
- いつエラーが発生したか
- どこでエラーが発生したか
- 何のエラーか
問題を後からデバッグするためには、イベントログやデータベース、その他のログファイルなどのように、エラーを集中的に記録するための場所が不可欠です(私はこれを法医学的デバッグと呼んでいます)。
IISには優れたエラー処理機能があります(詳細についてはhttp://www.15seconds.com/issue/020821.htmに掲載されている私の記事を参照)。しかし、この機能にはいくつか問題があります。エラーの発生を予想できたとしても、そのエラーを洗練された方法でトラップするためには、サイトの既定のエラーリダイレクトページをオーバーライドしなければならない場合があります(このオーバーライドはIISのカスタムエラーページ内で行います。詳細は上記の記事を参照)。たとえば、認証が必要なリソースへのアクセスでエラーが発生した場合は、アプリケーションのログインページにリダイレクトすることが考えられます。また、Webホスティングに関しても問題があります。通常は、ホスティングしているWebサイトのIIS設定を制御できません。そのため、従来のASPでカスタムエラーページをセットアップすることはほぼ不可能です。ASP.NETでは、この問題が解決されています。以降ではこれについて説明します。
ソリューション
問題はいくつかありますが、それに対するソリューションは非常に単純です。ASP.NETには、処理されないエラーへの対処を定義するための場所が3か所あります。
- 「web.config」ファイルの
customErrors
セクション内 - 「global.asax」ファイルの
Application_Error
サブルーチン内 - 「aspx」または関連する分離コードページの
Page_Error
サブルーチン内
エラー処理イベントの実際の順序は次のとおりです。
- 該当ページの
Page_Error
サブルーチン ―― これは既定の名前です。このサブルーチンはHandles MyBase.Error
を指定しているので、名前を自由に定義できます。 - 「global.asax」ファイルの
Application_Error
サブルーチン - 「web.config」ファイル
Page_Error
またはApplication_Error
でエラーのバブルアップを取り消すには、サブルーチン内でServer.ClearError
関数を呼び出します。詳しくは後述しますが、それぞれの方法にはそれぞれの用途があります。 アプリケーション内で発生した例外は、System.Exception
クラスを継承するオブジェクトとして扱うことができます。このオブジェクトは次のパブリックプロパティを持ちます。
プロパティ名 | 説明 |
HelpLink | この例外に関連付けられているヘルプファイルへのリンクを取得/設定します。 |
InnerException | 現在の例外を発生させたException インスタンスを取得します。 |
Message | 現在の例外について説明するメッセージを取得します。 |
Source | エラーを発生させたアプリケーションまたはオブジェクトの名前を取得/設定します。 |
StackTrace | 現在の例外がスローされた時点のコールスタックのフレームの文字列表現を取得します。 |
TargetSite | 現在の例外をスローしたメソッドを取得します。 |
Page_ErrorまたはOnErrorサブルーチンを使用する
エラー処理の第一の防衛線はページレベルで実装します。次のようにしてMyBase.Error
サブルーチンをオーバーライドすることができます(エディタ内でOverrides
イベントまたはBaseClass
イベントをクリックすると、Visual Studioがこのコードを完成させてくれます)。使用できる関数は2つあります(どちらか一方を使用します。一方だけが呼び出されるので、両方は機能しません)。
Private Sub Page_Error(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Error End Sub
または
Protected Overrides Sub OnError(ByVal e As System.EventArgs) End Sub
これらのサブルーチンでエラーを処理するのは簡単です。Server.GetLastError
を呼び出してエラーを返せばよいのです。特定のページにリダイレクトしたい場合は、ここでResponse.Redirect ("HandleError.aspx")
を呼び出します(リダイレクト先のページは自由に指定できます)。このエラー処理の方法は、さまざまな理由で優れています。
Application_Error
または「web.config」ファイル内のcustomErrors
セクションをオーバーライドしなければならない場合- 各ページに独自のエラー処理を実装しなければならない場合。特定の情報をログに記録して実行を継続しなければならない場合は、ロギングやその他の処理のコードをここに記述するだけで済みます。ここでエラー処理を取り消す必要がある場合(つまり
Application_Error
またはcustomErrors
に進まない場合)は、このサブルーチン内でServer.ClearError
を呼び出すだけです。
「global.asax」ファイルを使用する
「global.asax」ファイルでは、エラーに対する第二の防衛線を実装します。エラーが発生すると、Application_Error
サブルーチンが呼び出されます。私はいつもこのサブルーチン内でエラーログを記録するようにしています。それが最も機能的だからです。私が作成する.NETアプリケーションでは、たいていのカスタムエラーをページレベルで処理せずに、アプリケーションレベルで処理します。Server.GetLastError
にアクセスできる場所は、Page_Error
サブルーチンとApplication_Error
サブルーチンしかありません。
Page_Error
が呼び出された後に、Application_Error
サブルーチンが呼び出されます。Application_Error
サブルーチンでも、エラーログを記録したり、別のページにリダイレクトしたりできます。このサブルーチンについては、これ以上詳しく説明しません。これは基本的にPage_Error
と同じものであり、違いはページレベルではなくアプリケーションレベルで動作するという点だけです。
「web.config」ファイルを使用する
「web.config」ファイルのcustomErrors
要素は、処理されないエラーに対する最後の防衛線です。他のエラーハンドラ(Application_Error
サブルーチンやPage_Error
サブルーチンなど)がある場合は、それらが先に呼び出されます。それらのエラーハンドラでResponse.Redirect
またはServer.ClearError
が実行されていない場合は、「web.config」ファイル内で定義されているページに進みます。「web.config」ファイル内では、特定のエラーコード(500、404など)を処理したり、1つのページですべてのエラーを処理したりできます。これが、他の方法との大きな違いです(とはいえ、他の方法でも、さまざまなResponse.Redirect
を使用すれば同じことを実現できます)。では、「web.config」ファイルの中身を見てみましょう。customErrors
セクションの形式は次のとおりです。
<customErrors defaultRedirect="url" mode="On|Off|RemoteOnly"> <error statusCode="statuscode" redirect="url"/> </customErrors>
mode
属性に指定できる値の意味は次のとおりです。
On
を指定すると、カスタムエラーが有効になります。defaultRedirect
が指定されていない場合は、汎用エラーがユーザーに表示されます。
Off
を指定すると、カスタムエラーが無効になります。これにより、詳細エラーが表示されるようになります。
RemoteOnly
を指定すると、リモートクライアントにはカスタムエラーが表示され、ローカルホストにはASP.NETエラーが表示されるようになります。これが既定値です。
既定では、Webアプリケーションを作成したときのcustomErrors
セクションは次のようになっています。
<customErrors mode="RemoteOnly" />
この場合、ユーザーには汎用ページが表示されます。独自に作成したページにリダイレクトするには、これを次のように変更します。
<customErrors mode="On" defaultRedirect="error.htm" />
こうすると、すべてのエラーに対して「error.htm」ページが表示されるようになります。
特定のエラーだけを処理し、それ以外のエラーはエラーページにリダイレクトするようにしたい場合は、特別な処理をするエラーコードを次の方法で指定します。
<customErrors mode="On" defaultRedirect="error.htm"> <error statusCode="500" redirect="error500.aspx?code=500"/> <error statusCode="404" redirect="filenotfound.aspx"/> <error statusCode="403" redirect="authorizationfailed.aspx"/> </customErrors>
このソリューションには1つ問題があります。リダイレクトを行ったときに、リダイレクト先のページにエラー情報が渡されないのです。これは、IISは(.NET Frameworkを通じて)エラーページに対して古い単純なGET要求を実行するだけで、IIS組み込みのエラー処理のようにServer.Transfer
を行わないからです。
リダイレクト先のページで利用できる情報は、エラーが発生したURLだけです。このURLは、クエリ文字列「aspxerrorpath」に格納されます(例:http://localhost/ErrorHandling/error500.aspx?aspxerrorpath=/ErrorHandling/WebForm1.aspx)。この情報を利用できるのは、前述の2つの場所だけです。
customErrors
要素のおもしろい点は、別のサブディレクトリ用に別のエラーページを指定できるということです。
たとえば、ルートディレクトリの外に「Customers」というディレクトリがあるとします。このディレクトリは、ログインする顧客に固有のブランド情報を含んでいますが、独自のアプリケーション内には含まれていません。このような場合は、エラー用のページセットを別に定義する必要があるでしょう。ここで、redirect
属性に指定されるページ参照は、サイトのルートパスではなく「Customers」サブディレクトリを基準とする相対参照であることに注意してください。さらに、「MYDOMAIN\Customers」だけがこれらのファイルにアクセスできる、というセキュリティルールも追加することにします。これらのエラーに対するルールを定義した「web.config」ファイルは次のようになります。
<configuration> <system.web> ... ... </system.web> <!-- Configuration for the "Customers" subdirectory. --> <location path="Customers"> <system.web> <customErrors mode="On" defaultRedirect="error.htm"> <error statusCode="500" redirect="CustomerError500.aspx"/> <error statusCode="401" redirect="CustomerAccessDenied.aspx"/> <error statusCode="404" redirect="CustomerPageNotFound.htm"/> <error statusCode="403" redirect="noaccessallowed.htm"/> </customErrors> <authorization> <allow roles="MYDOMAIN\Customers" /> <deny users="*" /> </authorization> </system.web> </location>
defaultRedirect
を設定したという場合は、ルートレベルに定義した500エラーハンドラが呼び出されます。したがって、親ディレクトリにハンドラがある場合の話です。コードを使用する
本稿のために設定付きのサンプルアプリケーションを作成しておいたので、これを見てコードの設定方法を勉強してください。サンプルのzipファイルは、2つのプロジェクトを含んだソリューションです。
そのうちの1つは、さまざまなエラーを発生させるボタンを備えたWebプロジェクトです。このプロジェクトは、ページ、「global.asax」ファイル、「web.config」ファイルでのエラー処理の例も示しています。さらに、クエリアナライザで実行できる「DotNetErrorLog.sql」も含まれています。このクエリは、エラーログ「ASAP」を開始するためのデータベース(およびユーザー)を作成します。
「web.config」ファイルには次のセクションが含まれています。
<appSettings> <add key="ErrorLoggingLogToDB" value="True" /> <add key="ErrorLoggingLogToEventLog" value="True" /> <add key="ErrorLoggingLogToFile" value="True" /> <add key="ErrorLoggingConnectString" value="Initial Catalog=DotNetErrorLog;Data Source=localhost;Integrated Security=SSPI;" /> <add key="ErrorLoggingEventLogType" value="Application" /> <add key="ErrorLoggingLogFile" value="c:\ErrorManager.log" /> </appSettings>
ここでは、アプリケーションの具体的な設定を定義しています。これらの設定をレジストリに登録する必要はありません。したがって、アプリケーションを開発環境、統合環境、本番環境の間で移行するときに便利です。セキュリティを高めるために、.NETに暗号化クラスを組み込んでデータベース接続情報を暗号化し、プレーンテキストの接続文字列ではなく暗号化した情報を「web.config」ファイルに格納することもできますが、これは明らかに本稿のテーマから外れています。これらの設定の内容は次のとおりです。
- データベースへの記録に関する設定:
ErrorLoggingLogToDB
-True
に設定した場合は、エラー情報をデータベースに記録します。ErrorLoggingConnectString
- エラー記録用データベースに接続するための接続文字列です。- イベントログへの記録に関する設定:
ErrorLoggingLogToEventLog
- Trueに設定した場合は、エラー情報をイベントログに記録します。ErrorLoggingEventLogType
- 記録先となるイベントログの名前です(例:「System」、「Application」など)。Webエラー専用のログを作成することも可能です。大規模なサイトでは、専用のログが役に立つでしょう。- テキストファイルへの記録に関する設定:
ErrorLoggingLogToFile
-True
に設定した場合は、エラー情報をテキストファイルに記録します。ErrorLoggingLogFile
- 記録先となるファイルのパスです。
ログファイルまたはイベントログに記録される情報の例を次に示します。
-----------------12/20/2002 3:00:36 PM----------------- SessionID:qwyvaojenw1ad1553ftnesmq Form Data: __VIEWSTATE - dDwtNTMwNzcxMzI0Ozs+4QI35VkUBmX1qfHHH8i25a/4g4A=Button1 - Cause a generic error in the customer directory 1: Error Description:Exception of type System.Web.HttpUnhandledException was thrown. 1: Source:System.Web 1: Stack Trace: at System.Web.UI.Page.HandleError(Exception e) 1: at System.Web.UI.Page.ProcessRequestMain() 1: at System.Web.UI.Page.ProcessRequest() 1: at System.Web.UI.Page.ProcessRequest(HttpContext context) 1: at System.Web.CallHandlerExecutionStep.Execute() 1: at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) 1: Target Site:Boolean HandleError(System.Exception) 2: Error Description:Object reference not set to an instance of an object. 2: Source:ErrorHandling 2: Stack Trace: at ErrorHandling.WebForm2.Button1_Click(Object sender, EventArgs e) in C:\Inetpub\wwwroot\ErrorHandling\Customers\WebForm2.aspx.vb:line 26 2: at System.Web.UI.WebControls.Button.OnClick(EventArgs e) 2: at System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent (String eventArgument) 2: at System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) 2: at System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) 2: at System.Web.UI.Page.ProcessRequestMain() 2: Target Site:Void Button1_Click(System.Object, System.EventArgs)
まず日付と時刻が記録されます。次にユーザーのセッションIDが記録されます。このセッションIDは、従来のASPセッションIDとは見た目が大きく異なります(以前はすべて数字でした)。次の行には、ページ上の任意のフォームデータが含まれます。このデータは、ページ上に入力された情報がアプリケーションのエラーの原因である場合に特に役立ちます。行頭に「1」とある行は、1番目のエラーを表しています。これらの行は、エラーの説明、ソース、スタックトレース、およびエラーの原因になった関数を示しています。「2」で始まる行は、エラー1の前に生成されたエラーです。この例でのエラー2は、エラー1のInnerException
です。これは古いASPやVBにはなかった新しい概念であり、エラー情報を階層的に扱えるようにします。たとえば、あるエラーをトラップし、より具体的な情報を提供する新しいエラーとして再スローすることができます。
また、SMTPコンポーネントを使用してエラー発生時に電子メールを送信し、エラーにすばやく対処できるようにする、という使い方も考えられます。これを実現するには、上記のappSettings
セクションに電子メールアドレスの設定を追加し、メールで送信するテキストをCErrorLog.GetErrorAsString
で取得すれば済みます。
補足
ネットのどこかで、エラー発生時にはエラー番号を取得し、データベースから適切なメッセージをルックアップしてユーザーに表示するべきだ、という意見を見たことがあるのですが。
それもいいアイデアだと思います。ただ、その方法の問題は、予想外のデータベースエラーが発生した場合にエラーページが機能しなくなるという点です。
try-catchブロック内でリダイレクトを実行する方法を手短に教えてください。
コードのtry-catch
ブロック内で別のページにリダイレクトする場合は、ある条件下ではリダイレクトが失敗する可能性があるということに注意してください。Response.Redirect
は内部的にResponse.End
を呼び出し、それが問題になることがあるからです。この場合は、Response.Redirect("pagename.aspx",False)
を呼び出す必要があります。そうすれば、リダイレクト呼び出しを行いつつResponse.End
を呼び出さずに済むので、例外を回避できます。
皆さんも、いろいろなエラーログ機能を試してみてください。