Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

ASP.NETでのWebアプリケーションのエラー処理

エラー処理のカスタマイズとシンプルなロギング

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/05/23 00:00

ダウンロード ソースコード (39.8 KB)

エラーの発生を予想できたとしても、そのエラーを洗練された方法でトラップするためには、サイトの既定のエラーリダイレクトページをオーバーライドしなければならない場合があります。ASP.NETでは、従来のASPよりも洗練された方法でこの問題が解決されています。

はじめに

 どんなアプリケーションにもエラー処理が必要です。そのことは誰でも知っています。しかし、我々開発者は、処理されないエラーがクライアントマシン上で発生した場合に、それを知ることができません。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」にあります)。問題は、このようなエラーが発生しても我々は関知できず、サイトのルックアンドフィールとは関係のないエラーページが表示されてしまうということです。エラーは開発につきものですが、我々はできるだけエラーを排除し、きちんと処理するよう努めなければなりません。エラー処理のためには、次のことを知る必要があります。

  1. いつエラーが発生したか
  2. どこでエラーが発生したか
  3. 何のエラーか

 問題を後からデバッグするためには、イベントログやデータベース、その他のログファイルなどのように、エラーを集中的に記録するための場所が不可欠です(私はこれを法医学的デバッグと呼んでいます)。

 IISには優れたエラー処理機能があります(詳細についてはhttp://www.15seconds.com/issue/020821.htmに掲載されている私の記事を参照)。しかし、この機能にはいくつか問題があります。エラーの発生を予想できたとしても、そのエラーを洗練された方法でトラップするためには、サイトの既定のエラーリダイレクトページをオーバーライドしなければならない場合があります(このオーバーライドはIISのカスタムエラーページ内で行います。詳細は上記の記事を参照)。たとえば、認証が必要なリソースへのアクセスでエラーが発生した場合は、アプリケーションのログインページにリダイレクトすることが考えられます。また、Webホスティングに関しても問題があります。通常は、ホスティングしているWebサイトのIIS設定を制御できません。そのため、従来のASPでカスタムエラーページをセットアップすることはほぼ不可能です。ASP.NETでは、この問題が解決されています。以降ではこれについて説明します。

ソリューション

 問題はいくつかありますが、それに対するソリューションは非常に単純です。ASP.NETには、処理されないエラーへの対処を定義するための場所が3か所あります。

  1. 「web.config」ファイルのcustomErrorsセクション内
  2. 「global.asax」ファイルのApplication_Errorサブルーチン内
  3. 「aspx」または関連する分離コードページのPage_Errorサブルーチン内

 エラー処理イベントの実際の順序は次のとおりです。

  1. 該当ページのPage_Errorサブルーチン ―― これは既定の名前です。このサブルーチンはHandles MyBase.Errorを指定しているので、名前を自由に定義できます。
  2. 「global.asax」ファイルのApplication_Errorサブルーチン
  3. 「web.config」ファイル
 Page_ErrorまたはApplication_Errorでエラーのバブルアップを取り消すには、サブルーチン内でServer.ClearError関数を呼び出します。詳しくは後述しますが、それぞれの方法にはそれぞれの用途があります。

 アプリケーション内で発生した例外は、System.Exceptionクラスを継承するオブジェクトとして扱うことができます。このオブジェクトは次のパブリックプロパティを持ちます。

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")を呼び出します(リダイレクト先のページは自由に指定できます)。このエラー処理の方法は、さまざまな理由で優れています。

  1. Application_Errorまたは「web.config」ファイル内のcustomErrorsセクションをオーバーライドしなければならない場合
  2. 各ページに独自のエラー処理を実装しなければならない場合。特定の情報をログに記録して実行を継続しなければならない場合は、ロギングやその他の処理のコードをここに記述するだけで済みます。ここでエラー処理を取り消す必要がある場合(つまり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>
 開発していて気が付いたのは、これらのエラーには相続順位があるらしいということです。つまり、ルートサイトに500エラーを定義し、「Customers」ディレクトリにはエラーを何も定義せず、ただし「Customers」ディレクトリには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」ファイルに格納することもできますが、これは明らかに本稿のテーマから外れています。これらの設定の内容は次のとおりです。

  1. データベースへの記録に関する設定:
  2. ErrorLoggingLogToDB - Trueに設定した場合は、エラー情報をデータベースに記録します。
  3. ErrorLoggingConnectString - エラー記録用データベースに接続するための接続文字列です。
  4. イベントログへの記録に関する設定:
  5. ErrorLoggingLogToEventLog - Trueに設定した場合は、エラー情報をイベントログに記録します。
  6. ErrorLoggingEventLogType - 記録先となるイベントログの名前です(例:「System」、「Application」など)。Webエラー専用のログを作成することも可能です。大規模なサイトでは、専用のログが役に立つでしょう。
  7. テキストファイルへの記録に関する設定:
  8. ErrorLoggingLogToFile - Trueに設定した場合は、エラー情報をテキストファイルに記録します。
  9. 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を呼び出さずに済むので、例外を回避できます。

 皆さんも、いろいろなエラーログ機能を試してみてください。



  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • Adam Tuliper(Adam Tuliper)

    .NET、COM、VB、ASP、C、C++、およびSQL Serverによるビジネスソリューションとコンシューマアプリケーションの開発に従事。プログラム開発の仕事で6年以上のキャリアを持つ。インターネットセンターソフトウェアやシステム監視/セキュリティスキャンソフトウェアを開発するGecko Sof...

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5