ASP.NET Web API 錯誤處理

摘要:介紹 ASP.NET Web API 錯誤處理的程式寫法。

前言

上一篇 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。基本步驟是:
  1. 建立 HttpResponseMessage 物件,並指定欲傳回的狀態碼,例如 HttpStatusCode.BadRequest。
  2. 把錯誤訊息塞給 HttpResponseMessage 物件的 Content 屬性。如需額外解釋錯誤原因,還有個 ReasonPhase 屬性可用。
  3. 建立 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,亦即它本身就是個 key-value 集合。我們可以塞一些額外錯誤資訊進去,並傳回至用戶端。例如:
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。就留給延伸閱讀吧!

延伸閱讀

沒有留言:

技術提供:Blogger.
回頂端⬆️