miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET Web API カスタムエラーメッセージ

以前の記事ASP.NET WEB API エラーの対処についてで、エラー用フィルター属性を使った例外処理について書きました。
今回はそれに続いて、エラー時に返すレスポンスの Body を、カスタマイズする方法です。

エラーの詳細表示を切り替える

エラーが起きた時に返されるレスポンスの Body は、以前の記事でも書いた通り、
HttpConfiguration クラス IncludeErrorDetailPolicy プロパティの Enum の値、
(Default, LocalOnly, Always, Never)の値や、Web.config の設定に依存します。

  • IncludeErrorDetailPolicy が Default で、Web.config の customErrors mode が "Off" の場合

  • IncludeErrorDetailPolicy が Never の場合

独自で作成する場合も、これらの設定に対応するようにします。

作成したレスポンスの Body

作成したレスポンスは以下の様になります。
ちょっとわかりにくいですが、プロパティを Myなんとか にし、メッセージも変更してみました。

  • IncludeErrorDetailPolicy が Default で、Web.config の customErrors mode が"Off"の場合

  • IncludeErrorDetailPolicy が Never の場合

実装

実装は、エラー用フィルター属性を作成するだけです。
HttpError オブジェクトを作成し、CreateErrorResponse メソッドより
HttpResponseMessage を作成します。

using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Filters;
using System.Web.Http.Hosting;

namespace ExceptionTrain.Models
{
	/// <summary>
	/// 例外フィルター
	/// </summary>
	public class MyExceptionFilterAttribute : ExceptionFilterAttribute
	{
		public override void OnException(HttpActionExecutedContext actionExecutedContext)
		{
			Exception ex = actionExecutedContext.Exception;
			HttpRequestMessage request = actionExecutedContext.Request;

			HttpError myHttpError = CreateMyAppHttpError(ex, request);
			actionExecutedContext.Response = 
				request.CreateErrorResponse(HttpStatusCode.InternalServerError, myHttpError);
		}

		/// <summary>
		/// 独自のHttpErrorを作成します。
		/// </summary>
		/// <param name="ex"></param>
		/// <param name="request"></param>
		/// <returns></returns>
		private HttpError CreateMyAppHttpError(Exception ex, HttpRequestMessage request)
		{
			//エラー詳細を含めるかどうかを取得
			bool includeErrorDetail = IncludeErrorDetail(request);
			return CreateMyAppHttpError(ex, includeErrorDetail);
		}

		/// <summary>
		/// 独自のHttpErrorを作成します。
		/// </summary>
		/// <param name="ex"></param>
		/// <param name="includeErrorDetail"></param>
		/// <returns></returns>
		private HttpError CreateMyAppHttpError(Exception ex, bool includeErrorDetail)
		{
			//★2
			var httpError = new HttpError();

			httpError["MyMessage"] = String.Format(
				"エラーが発生しました。type : {0}", ex.GetType().FullName);

			//エラー詳細を含める場合、他の情報も取得。
			if (includeErrorDetail)
			{
				httpError["MyExeptionMessage"] = ex.Message;
				httpError["MyStackTrace"] = ex.StackTrace;
			}

			return httpError;
		}

		/// <summary>
		/// ★1 HttpRequestMessageより、エラーの詳細を含めるかどうかを取得します。
		/// </summary>
		/// <param name="request"></param>
		/// <returns></returns>
		private bool IncludeErrorDetail(HttpRequestMessage request)
		{
			//★3
			HttpConfiguration configuration = request.GetConfiguration();
			IncludeErrorDetailPolicy includeErrorDetailPolicy = IncludeErrorDetailPolicy.Default;
			if (configuration != null)
			{
				includeErrorDetailPolicy = configuration.IncludeErrorDetailPolicy;
			}
			switch (includeErrorDetailPolicy)
			{
				case IncludeErrorDetailPolicy.Default:
					Lazy<bool> includeErrorDetail = GetProperty<Lazy<bool>>(request, HttpPropertyKeys.IncludeErrorDetailKey);
					if (includeErrorDetail != null)
					{
						// If we are on webhost and the user hasn't changed the IncludeErrorDetailPolicy
						// look up into the Request's property bag else default to LocalOnly.
						return includeErrorDetail.Value;
					}

					goto case IncludeErrorDetailPolicy.LocalOnly;

				case IncludeErrorDetailPolicy.LocalOnly:
					Lazy<bool> isLocal = GetProperty<Lazy<bool>>(request, HttpPropertyKeys.IsLocalKey);
					return isLocal == null ? false : isLocal.Value;

				case IncludeErrorDetailPolicy.Always:
					return true;

				case IncludeErrorDetailPolicy.Never:
				default:
					return false;
			}
		}

		private static T GetProperty<T>(HttpRequestMessage request, string key)
		{
			object value;
			if (request.Properties.TryGetValue(key, out value))
			{
				if (value is T)
				{
					return (T)value;
				}
			}
			return default(T);
		}
	}
}

作ったフィルターを適用します。

//WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
  config.Filters.Add(new MyExceptionFilterAttribute());
  //...
}

コードは以上です。
以下★印について細々と。

★1 エラー詳細を含めるかどうかの取得について

デフォルトでは、HttpConfiguration クラスの ShouldIncludeErrorDetail メソッドに
エラー詳細を含めるかどうかのロジックがあります。
が、private メソッドなので、開発者はそれを利用することができません。

最近、それが修正された様で*1、HttpRequestMessage の拡張メソッドからそのロジックを利用することができるようになりました。
コード HttpRequestMessageExtensions クラス
が、現在の RTM では使えないので、実装ではそのコードをそのままコピペしました。

★2 HttpError について

HttpError は、HttpResponseMessage よりもモデルに近い、エラー情報をもつオブジェクトです。
デフォルトでは、Exception や ModelState 等から作成できるようになっています。
独自のエラーメッセージを作成する場合は、一度この HttpError で作成できるかどうかを検討した方が良さそうです。

元はただの Dictionary を継承したクラスでしたが、だんだん StackTrace や ExceptionType の
プロパティを装備するようになりました。(`・ω・´) シャキーン
HttpError.cs

★3 HttpConfiguration

心臓ともいえる設定情報をもつ HttpConfiguration ですが、フレームワーク内ではよく、
HttpRequestMessage から GetConfiguration メソッド で取得しています。
GlobalConfiguration.Configuration で取得するより、前者で取得した方が、テストの時等に便利そうです。

*1:HttpConfiguration.ShouldIncludeErrorDetail should be public http://aspnetwebstack.codeplex.com/workitem/361