在Web應用程式提供服務時,程式難免會遭遇到一些預期內或是預期外的錯誤,像是客戶端請求的網址沒有路由處理程序能夠處理、請求中的資料格式有誤(例如原本需要輸入數字的地方被輸入成字母文字)、授權逾期,或是資料庫突然連線失敗等,Web應用程式應該都要有能力避免應用程式崩潰(crash),並且照常發送HTTP回應。而Rocket框架當然預設就有處理這些錯誤的能力,開發者甚至可以利用自訂的錯誤捕獲者(Error Catcher),來覆蓋掉預設的處理方式。



再回頭以先前製作過的Hello World程式來舉例。當客戶端向Hello World應用程式發送GET /請求時,Hello World應用程式會回應Hello, world!。但當客戶端向Hello World應用程式發送GET /hi請求時,由於沒有路由處理程序能夠匹配這個網址路徑,所以就會觸發Rocket框架的錯誤捕獲者機制。

當傳送進來的HTTP請求沒有路由處理程序能夠匹配時,Rocket框架就會自動產生404(NotFound)錯誤,這個錯誤不會直接使用HTTP狀態碼來回應,而是會先去尋找框架中有沒有針對404自訂的錯誤捕獲者,如果沒有的話,就會使用Rocket預設的處理方式,將404錯誤以「404錯誤網頁」的方式回應。如下圖:

rocket-error-catcher

同樣地,其它類型的錯誤,Rocket預設也都會提供對應的處理方式。例如,當路由處理程序的函數發生panic或是回傳http::Status列舉的InternalServerError變體時,Rocket框架就會自動產生500錯誤,這個錯誤不會直接使用HTTP狀態碼來回應,而是會先去尋找框架中有沒有針對500自訂的錯誤捕獲者,如果沒有的話,就會使用Rocket預設的處理方式,將500錯誤以「500錯誤網頁」的方式回應。如下圖:

rocket-error-catcher

自訂錯誤捕獲者

我們可以利用自訂錯誤捕獲者,來讓Rocket以我們希望的方式來處理這些錯誤。例如發生404錯誤時,可以在瀏覽器上出現與網站風格相似的404錯誤頁面;或是在發生500錯誤時,顯示出稍候再嘗試的提示頁面。

Rocket的錯誤捕獲者的最基本架構如下:

#[catch(HTTP錯誤相關的狀態碼)]
fn error_catcher() { 處理請求的程式區塊 }

與路由處理程序的函數相同,錯誤捕獲者也可以設定要回應的資料型別,如下:

#[catch(HTTP錯誤相關的狀態碼)]
fn error_catcher() -> 要回應的資料型別 { 處理請求的程式區塊 }

錯誤捕獲者的函數所回傳的資料型別的用法和路由處理程序的函數是完全相同的。

例如,以下的錯誤捕獲者,可以建立出一個基本的404錯誤網頁:

use rocket::response::content::RawHtml;

#[catch(404)]
fn error_catcher() -> RawHtml<&'static str> {
    RawHtml(
        r#"<html>
    <head>
        <meta charset=UTF-8>
        <title>404 Page</title>
    </head>
    <body>
        Ooops! You got a wrong path.
    </body>
</html>"#,
    )
}

若想要在錯誤捕獲者中處理請求,可以替錯誤捕獲者中的函數,加入一個request::Request型別的參數。

例如:

use rocket::request::Request;

#[catch(404)]
fn error_catcher(req: &Request) -> String {
    format!("Sorry, '{}' is not a valid path.", req.uri())
}

註冊錯誤捕獲者

與路由處理程序相同,將錯誤捕獲者實作出來之後,也是要註冊給Rocket才能發揮作用。註冊錯誤捕獲者的方式與註冊路由處理程序的方式差不多,需用Rocket結構實體的register方法和rocketcatchers!巨集來註冊。

如以下程式:

use rocket::request::Request;

#[catch(404)]
fn error_catcher(req: &Request) -> String {
    format!("Sorry, '{}' is not a valid path.", req.uri())
}

#[launch]
fn rocket() -> _ {
    rocket::build().register("/", catchers![error_catcher])

    // ...
}

請求守衛與錯誤捕獲者

請求守衛與錯誤捕獲者可以直接搭配使用。在實作request::FromRequest特性時,使from_request關聯函數回傳OutcomeError變體,就可以使有使用到該請求守衛的路由處理程序在匹配失敗之後,直接回傳其所帶的http::Status結構實體,來觸發Rocket的錯誤捕獲者機制;而回傳OutcomeForward變體,則可以在該請求守衛的路由處理程序在匹配失敗後,將請求交給下一個路由處理程序但其也匹配失敗之後,回傳其所帶的http::Status結構實體,來觸發Rocket的錯誤捕獲者機制。

例如:

#[macro_use]
extern crate rocket;

use rocket::{
    http::Status,
    request::{FromRequest, Outcome, Request},
};

struct MyGuard<'a> {
    authorization: &'a str,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for MyGuard<'r> {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let authorization = request.headers().get("authorization").next();

        match authorization {
            Some(authorization) => Outcome::Success(MyGuard {
                authorization,
            }),
            None => Outcome::Failure((Status::Unauthorized, ())),
        }
    }
}

#[get("/auth")]
fn handler(guard: MyGuard) -> &str {
    guard.authorization
}

#[catch(401)]
fn error_catcher() -> &'static str {
    "You have no authorization to execute action."
}

#[launch]
fn rocket() -> _ {
    rocket::build().register("/", catchers![error_catcher]).mount("/", routes![handler])
}

以上程式,當MyGuard這個請求守衛回傳Outcome列舉的Failure變體,而導致handler這個路由處理程序匹配失敗時,就會等同於直接回傳http::Status列舉的Unauthorized變體,而產生401錯誤,進一步觸發error_catcher這個錯誤捕獲者來進行HTTP的回應。

總結

Rocket框架雖然預設就會幫我們處理HTTP狀態碼,但我們還是可以藉由向Rocket框架註冊我們自己實作的錯誤捕獲者,來改變Rocket框架對這些錯誤狀態的處理方式。在下一個章節中,將會介紹更進階的處理HTTP請求的方式。

下一章:進階處理HTTP請求(Request)的方式