ASP.NET Web API 錯誤處理
文章目錄
摘要:介紹 ASP.NET Web API 錯誤處理的程式寫法。
前言
上一篇 ASP.NET Web API 筆記裡面提到的幾個技巧,大略點出幾個入門的基礎知識,包括:建立 Web API、routing、撰寫動作方法、產生 JSON 回應等等。這次要學習的課題是錯誤處理(exception handling)。
用戶端收到的錯誤訊息
當 Web API 要通知用戶端有錯誤發生時,最簡單的方法就是丟出一個 exception。參考以下範例:
當錯誤發生時,用戶端會收到什麼結果呢?
ASP.NET 應用程式的 web.config 裡面有個 customErrors 元素,其 mode 屬性可用來控制是否要顯示自訂錯誤訊息。過去有寫過 ASP.NET 程式的人應該都很熟了。若將 customErrors 的 mode 屬性設定為 "On",會看到這樣的簡易錯誤訊息:
使用 Fiddler 觀察伺服器的回應內容,HTTP 狀態碼會是 500(內部伺服器錯誤),像這樣:
如果 web.config 中的 customErrors 的 mode 屬性為 "Off" 或 "RemoteOnly",那麼在本機存取此 Web API 時,會看到更詳細的訊息:
以上描述適用於 Chrome 。若使用 IE,則預設會顯示易懂的 HTTP 錯誤訊息,也就是看不到應用程式實際丟出來的錯誤訊息,如下圖:
如果將 IE 的「顯示易懂的 HTTP 錯誤訊息」選項關閉,IE 還是不會顯示訊息,因為我們的 Web API 傳回的內容是 JSON 類型的文件,IE 會問你要開啟還是下載檔案。底下的範例可以在伺服器端拒絕 IE 顯示易懂訊息的好意,也能避免 IE 詢問要開啟還是下載錯誤訊息:
前面幾行有個迴圈,是用來把錯誤訊息膨脹至超過 500 bytes,如此 IE 便不會顯示易懂的錯誤訊息。奇妙吧?
用 IE 查看執行結果:
這小撇步或許談不上實用,只是好奇,實驗一下而已。
不過,剛才範例程式中的最後一行所拋出的例外是 HttpResponseException,這就與本文主題有關了。
HttpResponseException
同樣是拋出 exception,使用基礎類別 Exception 和使用 HttpResponseException 的主要差異就在於 HttpResponseException 可以讓我們指定 HTTP 狀態碼,例如 404、503 等等。如果拋出的錯誤型別是 Exception,用戶端就只會收到預設的 HTTP 500 錯誤。
其實在上一個範例程式中,已經有示範如何使用 HttpResponseException。基本步驟是:
瀏覽器的顯示結果接收到的回應內容會像這樣:
與第一個範例的執行結果比較,有下列差異:
現在我們知道,HttpResponseException 其實是把 HttpResponseMessage 包起來,再傳回用戶端。咦....我們的 Web API 方法不是已經可以傳回 HttpResponseMessage 了嗎?那麼,傳回錯誤時是不是也可以這樣寫:
沒錯,這種寫法對用戶端而言,產生的結果與 throw new HttpResponseException(...) 完全一樣。可是,如果我們的 Web API 方法是要傳回一個自訂類別而不是 HttpResponseMessage,例如本文第一個範例程式傳回的 Customer,那就得用拋出例外的方式,也就是 throw new HttpResponseException() -- 或使用 HttpError。
HttpError
如果用戶端並非瀏覽器,而是其他應用程式,例如手機 app,那麼對方就會需要從回應結果中取出錯誤訊息。因此,Web API 傳回錯誤訊息的時候,格式最好一致,以便用戶端應用程式解讀。
HttpError 的主要用途就是讓我們在撰寫 Web API 的錯誤處理程式時,能夠採用一致的寫法、一致的錯誤訊息格式。參考底下的範例:
此範例的回傳型別雖然還是 HttpResponseMessage,但是在沒有錯誤發生的情況下,它是利用 Request.CreaetResponse() 方法來傳回序列化之後的 Customer 物件。這裡有個不那麼明顯的好處:其序列化的作法與 ASP.NET Web API 內建的 content-negotiation 與序列化的程序完全一樣(就跟本文第一個範例宣告回傳強型別的 Customer 物件一樣)。
如果發生錯誤,則使用 Request.CreateErrorResponse() 方法來傳回 HttpError 物件。結果用戶端會收到回應如下:
它也是個 JSON 字串。
剛才的範例程式,處理錯誤的部分還可以簡化為:
這樣還是有用到 HttpError,因為 CreateErrorResponse 方法會在內部建立一個 HttpError 物件,並將它包在回傳的 HttpResponseMessage 物件中。
CreateResponse() 和 CreateErrorResponse() 都是擴充方法,由 HttpRequestMessageExtensions 類別提供。
使用 HttpError 傳回額外錯誤資訊
HttpError 係繼承自 Dictionary,亦即它本身就是個 key-value 集合。我們可以塞一些額外錯誤資訊進去,並傳回至用戶端。例如:
用戶端會得到:
HttpError 搭配 HttpResponseException
除了前面介紹的幾種寫法,HttpError 還可以和 HttpResponseException 一起搭配使用,讓 Web API 方法的回傳型別不受限於 HttpResponseMessage,並使用拋出例外的方式傳回一致的錯誤訊息格式。
最後來張大合照:
小結
這篇筆記還有兩個議題沒照顧到:exception filter 和 model validation。就留給延伸閱讀吧!
延伸閱讀
前言
上一篇 ASP.NET Web API 筆記裡面提到的幾個技巧,大略點出幾個入門的基礎知識,包括:建立 Web API、routing、撰寫動作方法、產生 JSON 回應等等。這次要學習的課題是錯誤處理(exception handling)。
用戶端收到的錯誤訊息
當 Web API 要通知用戶端有錯誤發生時,最簡單的方法就是丟出一個 exception。參考以下範例:
[HttpGet]
public Customer Get(int id)
{
if (id > 10)
{
throw new Exception("Parameter id must be specified!");
}
return new Customer() { FirstName = "Mciahel", LastName = "Tsai" };
}
當錯誤發生時,用戶端會收到什麼結果呢?
ASP.NET 應用程式的 web.config 裡面有個 customErrors 元素,其 mode 屬性可用來控制是否要顯示自訂錯誤訊息。過去有寫過 ASP.NET 程式的人應該都很熟了。若將 customErrors 的 mode 屬性設定為 "On",會看到這樣的簡易錯誤訊息:
{"Message":"An error has occurred."}
使用 Fiddler 觀察伺服器的回應內容,HTTP 狀態碼會是 500(內部伺服器錯誤),像這樣:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
Content-Length: 36
{"Message":"An error has occurred."}
如果 web.config 中的 customErrors 的 mode 屬性為 "Off" 或 "RemoteOnly",那麼在本機存取此 Web API 時,會看到更詳細的訊息:
{
"Message":"An error has occurred.",
"ExceptionMessage":"Parameter id must be specified!",
"ExceptionType":"System.Exception","StackTrace":" ...略... cancellationToken)"
}
以上描述適用於 Chrome 。若使用 IE,則預設會顯示易懂的 HTTP 錯誤訊息,也就是看不到應用程式實際丟出來的錯誤訊息,如下圖:
如果將 IE 的「顯示易懂的 HTTP 錯誤訊息」選項關閉,IE 還是不會顯示訊息,因為我們的 Web API 傳回的內容是 JSON 類型的文件,IE 會問你要開啟還是下載檔案。底下的範例可以在伺服器端拒絕 IE 顯示易懂訊息的好意,也能避免 IE 詢問要開啟還是下載錯誤訊息:
[HttpGet]
public HttpResponseMessage IENoFriendlyError()
{
// 抑制 IE 好心的「顯示易懂的 HTTP 錯誤訊息」
StringBuilder sb = new StringBuilder("Parameter id must be specified!");
sb.Append("<!--");
for (int i = 0; i < 500; i++) { sb.Append("x"); }
sb.Append("-->");
var respMsg = new HttpResponseMessage(HttpStatusCode.BadRequest);
respMsg.Content = new StringContent(sb.ToString());
respMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
throw new HttpResponseException(respMsg);
}
前面幾行有個迴圈,是用來把錯誤訊息膨脹至超過 500 bytes,如此 IE 便不會顯示易懂的錯誤訊息。奇妙吧?
用 IE 查看執行結果:
這小撇步或許談不上實用,只是好奇,實驗一下而已。
不過,剛才範例程式中的最後一行所拋出的例外是 HttpResponseException,這就與本文主題有關了。
HttpResponseException
同樣是拋出 exception,使用基礎類別 Exception 和使用 HttpResponseException 的主要差異就在於 HttpResponseException 可以讓我們指定 HTTP 狀態碼,例如 404、503 等等。如果拋出的錯誤型別是 Exception,用戶端就只會收到預設的 HTTP 500 錯誤。
其實在上一個範例程式中,已經有示範如何使用 HttpResponseException。基本步驟是:
- 建立 HttpResponseMessage 物件,並指定欲傳回的狀態碼,例如 HttpStatusCode.BadRequest。
- 把錯誤訊息塞給 HttpResponseMessage 物件的 Content 屬性。如需額外解釋錯誤原因,還有個 ReasonPhase 屬性可用。
- 建立 HttpResponseException 物件,並將 HttpResponseMessage 物件包進去,然後用 throw 來拋出這個 exception 物件。
瀏覽器的顯示結果接收到的回應內容會像這樣:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
Content-Length: 31
Parameter id must be specified!
與第一個範例的執行結果比較,有下列差異:
- HTTP 狀態碼是應用程式指定的 400,而不是預設的 500。
- 用戶端瀏覽器會顯示由應用程式指定拋出的錯誤訊息,而不是 "An error has occurred."。
- 錯誤訊息是單純的文字格式,而不是 JSON。
現在我們知道,HttpResponseException 其實是把 HttpResponseMessage 包起來,再傳回用戶端。咦....我們的 Web API 方法不是已經可以傳回 HttpResponseMessage 了嗎?那麼,傳回錯誤時是不是也可以這樣寫:
[HttpGet]
public HttpResponseMessage Demo2()
{
var respMsg = new HttpResponseMessage(HttpStatusCode.BadRequest);
respMsg.Content = new StringContent("Parameter id must be specified!");
return respMsg;
}
沒錯,這種寫法對用戶端而言,產生的結果與 throw new HttpResponseException(...) 完全一樣。可是,如果我們的 Web API 方法是要傳回一個自訂類別而不是 HttpResponseMessage,例如本文第一個範例程式傳回的 Customer,那就得用拋出例外的方式,也就是 throw new HttpResponseException() -- 或使用 HttpError。
HttpError
如果用戶端並非瀏覽器,而是其他應用程式,例如手機 app,那麼對方就會需要從回應結果中取出錯誤訊息。因此,Web API 傳回錯誤訊息的時候,格式最好一致,以便用戶端應用程式解讀。
HttpError 的主要用途就是讓我們在撰寫 Web API 的錯誤處理程式時,能夠採用一致的寫法、一致的錯誤訊息格式。參考底下的範例:
[HttpGet]
public HttpResponseMessage Demo3(int id)
{
if (id > 100)
{
HttpError err = new HttpError("Parameter id must be specified!");
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, err);
}
var customer = new Customer() { FirstName = "Mciahel", LastName = "Tsai" };
return Request.CreateResponse(HttpStatusCode.OK, customer);
}
此範例的回傳型別雖然還是 HttpResponseMessage,但是在沒有錯誤發生的情況下,它是利用 Request.CreaetResponse() 方法來傳回序列化之後的 Customer 物件。這裡有個不那麼明顯的好處:其序列化的作法與 ASP.NET Web API 內建的 content-negotiation 與序列化的程序完全一樣(就跟本文第一個範例宣告回傳強型別的 Customer 物件一樣)。
如果發生錯誤,則使用 Request.CreateErrorResponse() 方法來傳回 HttpError 物件。結果用戶端會收到回應如下:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
Content-Length: 45
{"Message":"Parameter id must be specified!"}
它也是個 JSON 字串。
剛才的範例程式,處理錯誤的部分還可以簡化為:
string msg = "Parameter id must be specified!";
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, msg);
這樣還是有用到 HttpError,因為 CreateErrorResponse 方法會在內部建立一個 HttpError 物件,並將它包在回傳的 HttpResponseMessage 物件中。
CreateResponse() 和 CreateErrorResponse() 都是擴充方法,由 HttpRequestMessageExtensions 類別提供。
使用 HttpError 傳回額外錯誤資訊
HttpError 係繼承自 Dictionary
HttpError err = new HttpError("Parameter id must be specified!");
err["AppErrorCode"] = 99;
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, err);
用戶端會得到:
HttpError 搭配 HttpResponseException
除了前面介紹的幾種寫法,HttpError 還可以和 HttpResponseException 一起搭配使用,讓 Web API 方法的回傳型別不受限於 HttpResponseMessage,並使用拋出例外的方式傳回一致的錯誤訊息格式。
[HttpGet]
public Customer Demo4(int id)
{
if (id > 10)
{
string msg = "Parameter id must be specified!";
throw new HttpResponseException(
Request.CreateErrorResponse(HttpStatusCode.BadRequest, msg));
}
var customer = new Customer() { FirstName = "Mciahel", LastName = "Tsai" };
return customer;
}
最後來張大合照:
小結
這篇筆記還有兩個議題沒照顧到:exception filter 和 model validation。就留給延伸閱讀吧!
延伸閱讀
- Exception Handling in ASP.NET Web API by Mike Wasson, March 12, 2012
- Model Validation by Mike Wasson, July 20, 2012
- ASP.NET Web API Exception Handling by Fredrick Norman, June 11, 2012
- ASP.NET Web API Content Negotiation and Accept-Charset by Henrik F Nielsen, April 22, 2012