2013年3月22日金曜日

ASP.NET MVC のエラーハンドリング(未完)

ASP.NET MVC でのエラーハンドリングを見直す機会があったのでメモします。

これまで

今までエラーハンドリングは customErrors の設定と、Application_Error イベントで行なっていました。

<customErrors mode="RemoteOnly" defaultRedirect="~/Error">
  <error statusCode="404" redirect="~/Error/NotFound"/>
</customErrors>
protected void Application_Error()
{
 var exception = Server.GetLastError();
 // 例外をロギングしたり...
}

こんな感じでエラーページの表示と適当に例外を起こすアクションを用意しました。

public class ErrorController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult NotFound()
    {
        return View();
    }

    public ActionResult Raise(int? httpCode)
    {
        if (!httpCode.HasValue)
            throw new ApplicationException("エラーだよ。");

        throw new HttpException(
            httpCode.Value, string.Format("ステータスコード {0} のエラーだよ。", httpCode));
    }
}

これで例外が起きると用意したエラーページが表示され、Application_Error で必要な処理(ロギングとか)を実行できます。

なにが問題?

この方法でもひと通りのことはできているんですが、よく見ると気になることがあります。

例外を起こしてエラーページを表示させてみます。このときの HTTP はこんな風になっています。

エラーページのステータスコードが "200"

エラーページなのにステータスコードが 200 になっています。
できれば 500 やその他適切なステータスコードを返したいところです。

リダイレクトによるエラーページのナビゲート

304 で customErrors で指定したエラーページの URL にリダイレクトしています。
これもできれば、リダイレクトせずにエラーページを表示できるほうが理想的です。

というわけで、"適切なステータスコード""リダイレクトせずに" エラーページを表示する方法を探しました。

エラーページのステータスコード

エラーページがそれぞれ適切なステータスコードで返るようにするのは簡単です。
Response.StatusCode を設定してあげれば OK です。

public class ErrorController : Controller
{
    public ActionResult Index()
    {
        Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        return View();
    }

    public ActionResult NotFound()
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        return View();
    }

    public ActionResult Raise ...
}

これだけでこんな感じで設定したステータスコードでレスポンスが返ります。

リダイレクトせずにエラーページを表示する

次はリダイレクトせずにエラーページを表示する方法です。

customErrors の redirectMode="ResponseRewrite" ... ?

customerErrors には redirectMode というプロパティがあり、 リダイレクトしてエラーページをナビゲートする "ResponseRedirect" かレスポンスをエラーページの内容で上書きして表示する "ResponseRewrite" かを設定できます。

<customErrors mode="On" defaultRedirect="~/Error" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Error/NotFound"/>
</customErrors>
残念ながら ASP.NET MVC ではうまく動かない

まさにコレ!という内容のプロパティですが、ASP.NET MVC では思ったように動作しません

ResponseRewrite ではレスポンスの内容を指定されたページの内容で上書きしてくれるのですが、 この際 ASP.NET は指定されたパスで物理ファイルを探しにいってしまいます。

なので、WebForm であればこれはうまく動作する(ハズ)のですが、ASP.NET MVC では動作しません。ざんねん...

物理ファイルで

物理ファイル探しにいくなら、物理ファイルで指定したら動くわけで... 指定しちゃってみます。

適当にエラーページを aspx で用意して、customErrors で指定します。
このとき aspx にはステータスコードを指定するコードを入れておくと任意のステータスコードでレスポンスを返せます。

protected override void OnLoad(EventArgs e)
{
    Response.StatusCode = (int)HttpStatusCode.InternalServerError;   
    base.OnLoad(e);
}
<customErrors mode="On" defaultRedirect="~/Views/Error/Index.aspx" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Views/Error/NotFound.aspx"/>
</customErrors>

動きますね。

これでいいのか

「レイアウト(_Layout.cshtml)とか使えないじゃん、コレ」とか「エラーページも Razor で書こうよ。」とか...

IIS7とかであれば httpErrors を使ってできたりするようです。

また、カスタムの HandleError を作ることでもできます。が、HandleError はあくまで ActionFilter なので、アクションメソッドの外側で起きるエラーには手出しできません。

一番いいのは ASP.NET MVC が redirectMode="ResponseRewrite" のときに物理ファイルをアテにするのではなく、パスの実行結果を上書きしてくれるようになることなんですが...

ここで時間切れ

いろいろ調べてみたんですが、なかなかコレ!っていう解決策が見つからない&思いつきませんでした。
もしいい方法をご存知の方がいらしたら教えて下さい。

TFT 10.14 Peeba Comp

こちらのガイドの自分用まとめです。 https://www.reddit.com/r/CompetitiveTFT/comments/hraunp/tft_1014_break_the_meta_new_peeba_comp_set_35/ 難しいですが完成すると非常に強く、プレ...