一個Web框架最基本的功能除了提供HTTP伺服器之外,還需要進行HTTP請求(request)的路由(routing)。Rocket在這方面套用了Rust程式語言提供的程序式巨集功能,使我們可以很簡單地使用getputpostdeleteheadpatchoptions等屬性來快速做出屬於該HTTP請求方法的路由處理程序(handler)。另外,我們也可以在撰寫屬性的同時,去對請求中的網址路徑(path)、網址查詢字串(query)、以及HTTP主體(body)選擇要使用的處理程序,以及進行資料型別的匹配。此外,Rocket也會負責檢查開發者撰寫的這些規則有沒有發生衝突,確保程式在編譯完成並啟動Rocket後,可以正確無誤地提供路由處理的服務。



上一章我們所撰寫的Hello World程式,當時使用的路由處理程序只有以下這個:

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

Rocket的路由處理程序的最基本架構如下:

#[HTTP請求方法("路徑")]
fn handler() { 處理請求的程式區塊 }

注意,上面的架構,handler不會在回應(respond)請求時,於HTTP主體中傳入任何資料。如果想要回傳資料,就得像剛才提到的Hello World程式的寫法一樣,替路由處理程序的函數加上回傳型別。如下:

#[HTTP請求方法("路徑")]
fn handler() -> 要回應的資料型別 { 處理請求的程式區塊 }

HTTP請求方法

HTTP請求方法的部份可以使用getputpostdeleteheadpatchoptions等屬性,分別對應著HTTP不同的請求方法。

Rocket也會自動幫助開發者進行常見的擴充。像是使用get屬性的話,Rocket也會自動實作head的功能。再來由於網頁表單只能夠送出GET或是POST請求,所以若是遇到內容類型(Content-Type)為application/x-www-form-urlencoded的POST請求時,Rocket就會去看這個請求的主體中的第一個欄位是否是_method,並且去檢查這個_method欄位中的值是否為PUT或是DELETE(大小寫可忽略),如果是的話,Rocket就會改以使用put或是delete屬性的路由處理程序來處理這個POST請求。

路徑

在屬性中所撰寫的路徑,除了可以寫固定(靜態)路徑之外,還可以使用變數來匹配路徑的任意組成部份(component),接著就可以在路由處理程序的函數參數中,替這個變數指定一個型別,使Rocket可以自動對其進行型別轉換與匹配。

例如:

#[get("/<name>")]
fn handler(name: String) -> String {
    name
}

以上這個路由處理程序,可以讓Rocket在處理路徑為/xxx的GET請求時,直接以路徑中的xxx來回應。

路徑中的變數,除了可以使用String型別之外,Rust內建的其它基本資料型別(如i8u16f32bool等)也都可以使用。這些型別稱為「參數守衛」(Parameter Guard),用來匹配路徑中的變數。如果想要將自己實作的型別當作參數守衛來使用,就必須實作rocket這個crate所提供的request::FromParam特性。

如果想要用一個變數來代替多個路徑的組成部份,則路徑在屬性中的寫法就會需要在變數名稱後面加上兩個半形句點..,且用來進行型別轉換與匹配的型別,就不能夠使用參數守衛,而是要使用「區段守衛」(Segments guard)。此外,區段守衛對應的變數,必須要寫在路徑的最尾端。

Rust標準函式庫提供的PathBuf可以直接作為區段守衛來使用,例如:

use std::path::PathBuf;

#[get("/<name..>")]
fn handler(name: PathBuf) -> String {
    name.to_str().unwrap().to_string()
}

如果想要將自己實作的型別當作區段守衛來使用,就必須實作rocket這個crate所提供的request::FromSegments特性。不過通常我們不會自己實作區段守衛啦!PathBuf就有很好的效能和安全性了。

路由的順序與轉遞(Forwarding)

同一種路徑可以撰寫多個不同的路由處理程序。

例如:

#[get("/<name>")]
fn handler_1(name: String) -> String {
    name
}

#[get("/<id>")]
fn handler_2(id: i32) -> String {
    format!("id = {}", id)
}

當我們把以上兩個路由處理程序都註冊給Rocket後,在使用Rocket結構實體的launch方法來啟動Rocket時,Rocket會檢查出路由設定發生衝突了,因為/<name>/<id>是一模一樣的匹配樣本(pattern)。

此時我們可以替這兩個路由處理程序手動設定優先順序,避免發生衝突。

添加優先順序的方式為,在HTTP請求方法的屬性參數中,於路徑後面,再多加一個rank參數,來指定一個大於等於0的整數值作為優先順序,寫法如下:

#[HTTP請求方法("路徑", rank = 優先順序的值)]
fn handler() -> 要回應的資料型別 { 處理請求的程式區塊 }

優先順序的值,數值愈大,表示優先權愈低。再回到剛才的兩個路由處理程序的例子,由於i32可表示的資料範圍比String小,因此handler_2處理程序的優先權應該要設得比handler_1處理程序的優先權還要高。

於是,我們將程式改寫如下:

#[get("/<name>", rank = 2)]
fn handler_1(name: String) -> String {
    name
}

#[get("/<id>", rank = 1)]
fn handler_2(id: i32) -> String {
    format!("id = {}", id)
}

當有/xxx的GET請求進來時,會先以handler_2處理程序來進行型別轉換與匹配,如果因xxx無法轉換為i32型別而匹配失敗了,就會將該請求轉遞給下一個,也就是handler_1處理程序來進行型別轉換與匹配。

在我們不明確使用rank來指定優先順序的值時,Rocket會根據路徑的匹配樣本類型,來決定該路由處理程序的優先順序的預設值。這部份會在這篇文章之後再詳細說明。

網址查詢字串

網址查詢字串,也就是網址中問號?後面的部份也可以被寫進路由處理程序的路徑匹配樣本中。例如將Hello World程式使用的路由處理程序改成以下這樣:

#[get("/?hello")]
fn index() -> &'static str {
    "Hello, world!"
}

當Rocket接收到的GET請求的網址是/時,且網址查詢字串中一定要有hello,才會在網頁上印出Hello, world!。如果GET請求中的網址查詢字串是?hello=xxx,匹配將會失敗。如果網址查詢字串還有其它的欄位,例如?hello&a&b=1,是可以匹配成功的!

如果有多個網址查詢字串的欄位要進行匹配,可以直接寫在路徑的匹配樣本中,用&來分隔。例如:

#[get("/?hello&a&b=1")]
fn index() -> &'static str {
    "Hello, world!"
}

網址查詢字串中的欄位順序可以忽略。如以上的路由處理程序,無論遇到?hello&a&b=1還是?a&hello&b=1,都可以成功匹配!

另外,網址查詢字串中的欄位也可以利用變數來進行型別的匹配與轉換。例如:

#[get("/?hello&<id>")]
fn index(id: i32) -> String {
    format!("Hello, world! id = {}", id)
}

以上這個路由處理程序,可以讓Rocket在處理路徑為/的GET請求時,去檢查其網址查詢字串中有無helloid欄位,且id欄位的值必須要可以轉成i32型別。

網址查詢字串中的變數,除了可以使用i32型別之外,String和Rust內建的其它基本資料型別(如i8u16f32bool等)也都可以使用。如果想要將自己實作的型別也拿來匹配網址查詢字串,就必須實作rocket這個crate所提供的request::FromFormValue特性。

另外,網址查詢字串也有類似路徑的區段守衛的機制,稱為「查詢守衛」(Query Guard)。我們可以使用一個變數來代替多個網址查詢字串的欄位,在屬性中的寫法就會需要在變數名稱後面加上兩個半形句點..,且用來進行型別轉換與匹配的型別,就不能夠單純使用有實作request::FromFormValue特性的型別,而是要使用實作request::FromQuery特性的型別。此外,查詢守衛對應的變數,必須要寫在網址查詢字串的最尾端。

我們不太會直接使用impl關鍵字來替我們的型別實作request::FromQuery特性,而是會直接替我們的型別加上FromForm這一個derive屬性的參數。這個程序式巨集會直接使用結構體的欄位名稱和型別來匹配網址查詢字串中的欄位和其對應的資料,欄位的型別必須要實作request::FromFormValue特性。接著在路由處理程序函數的參數中,使用實作了request::FromQuery特性的request::Form<T>或是request::LenientForm<T>來包裹我們用程序式巨集來實作request::FromForm特性的型別Trequest::Form<T>表示請求中的網址查詢字串的剩餘欄位必須要符合型別T的欄位結構,且不能有多出來的欄位;而request::LenientForm<T>表示請求中的網址查詢字串的剩餘欄位必須要符合型別T的欄位結構,但允許有多出來、沒有被匹配到的欄位。

如以下程式:

use rocket::request::Form;

#[derive(FromForm)]
struct User {
    id: i32,
    name: String,
}

#[get("/?hello&<user..>")]
fn index(user: Form<User>) -> String {
    format!("Hello, world! id = {}, name = {}", user.id, user.name)
}

以上這個路由處理程序,可以讓Rocket在處理路徑為/的GET請求時,去檢查其網址查詢字串中有無helloidname欄位,且id欄位的值必須要可以轉成i32型別,name欄位的值必須要可以轉成String型別。此外,除了剛才提到的欄位,如果請求中的網址查詢字串還有其它的欄位,則這個路由處理程序無法成功匹配。

不過,有時候我們可能會希望Rocket不要直接使用結構體中的欄位名稱來對應網址查詢字串中的欄位名稱,Rocket允許開發者針對結構體中的某個欄位,透過程序式巨集來手動指定要對應到哪個網址查詢字串中的欄位。例如將以上例子的User結構體改為:

#[derive(FromForm)]
struct User {
    #[form(field = "user_id")]
    id: i32,
    name: String,
}

以上程式,我們將User結構體的id欄位加上form屬性,並用field參數來指定要其對應到網址查詢字串中的user_id欄位。

路由處理程序的優先順序的預設值

下表是Rocket根據每個路由處理程序的路徑匹配樣本所預設的優先順序值:

路徑類型查詢類型Rank值路徑匹配樣本範例
靜態完全靜態或部份靜態-6/hello?world=true
靜態完全動態-5/hello?<world>
靜態不指定-4/hello
動態完全靜態或部份靜態-3/<hello>?world=true
動態完全動態-2/<hello>?<world>
動態不指定-1/<hello>

HTTP請求中的其它資料來源

HTTP請求中的資料可透過請求的網址路徑(params)、網址的查詢字串(query)、HTTP的標頭(headers)、HTTP的Cookie和HTTP的主體(body)中來取得。在上面的文章中,我們已經知道如何取得來自路徑和網址查詢字串的資料,在這個小節中,會再繼續探討剩下的部份。

標頭

使用Rocket取得HTTP標頭的資料屬於相對比較低階的操作,稍微麻煩了一點。我們會需要實作rocket這個crate所提供的request::FromRequest特性,使我們的型別能夠當作「請求守衛」(Request Guard)來使用。

一個最基本的request::FromRequest特性的實作方式如下:

use rocket::Outcome;
use rocket::request::{Outcome as RequestOutcome, Request, FromRequest};

struct MyStruct;

impl<'a, 'r> FromRequest<'a, 'r> for MyStruct {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> RequestOutcome<Self, Self::Error> {
        Outcome::Success(MyStruct)
    }
}

我們可以在from_request這個關聯函數中,利用request參數,來取得這個HTTP請求中的所有資料。也就是說,如果想要追求程式效能的話,先前提到的什麼參數守衛、區段守衛、查詢守衛都可以不要使用,全都改用一個請求守衛來完成。不過在這篇文章中只會介紹如何用request::Request結構實體來取得HTTP標頭的資料。

request::Request結構實體提供了一個headers方法,會回傳一個以Map作為資料結構的結構實體的不可變參考(&header::HeaderMap<'r>),這個結構體的用法就跟標準函式庫的HashMap差不多。Map結構所儲存的值一律使用型別為字串切片&str的鍵值來查詢(大小寫可忽略),查詢到的值會是一個迭代器(因為HTTP標頭的欄位可能會有重複的現象)。

舉個例子來說明,如果我們要取得請求中的Authorization標頭欄位,並將欄位的值交給請求守衛的結構實體來儲存,以便在路由處理程序中使用。程式實作如下:

use rocket::Outcome;
use rocket::request::{Outcome as RequestOutcome, Request, FromRequest};

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

impl<'a, 'r> FromRequest<'a, 'r> for MyGuard<'a> {
    type Error = ();

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

        Outcome::Success(MyGuard {
            authorization
        })
    }
}

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

如果想要強制讓接收的HTTP請求中一定要有Authorization標頭欄位的話,可以將以上程式修改如下:

use rocket::Outcome;
use rocket::request::{Outcome as RequestOutcome, Request, FromRequest};

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

impl<'a, 'r> FromRequest<'a, 'r> for MyGuard<'a> {
    type Error = ();

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

        match authorization {
            Some(authorization) => Outcome::Success(MyGuard {
                authorization
            }),
            None => Outcome::Forward(())
        }
    }
}

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

Cookie

Rocket支援Cookie和基於Cookie的會話(Cookie-based Session)。我們可以直接使用rocket這個crate所提供的http::Cookies結構體,當作請求守衛來使用。這個http::Cookies結構體可以對Cookie進行新增(修改)、查詢、刪除等操作。

例如以下程式,可以取得Cookie中的message這個欄位的資料。

use rocket::http::Cookies;

#[get("/cookie/message")]
fn handler(cookies: Cookies) -> String {
    cookies.get("message").map(|c| c.value()).unwrap_or("").to_string()
}

例如以下程式,可以設定Cookie中的message這個欄位的資料。

use rocket::http::{Cookie, Cookies};

#[put("/cookie/message")]
fn handler(mut cookies: Cookies) {
    cookies.add(Cookie::new("message", "Hello!"));
}

例如以下程式,可以移除Cookie中的message這個欄位。

use rocket::http::{Cookie, Cookies};

#[delete("/cookie/message")]
fn handler(mut cookies: Cookies) {
    cookies.remove(Cookie::named("message"));
}

如果要使用加密的Cookie,也就是基於Cookie的會話。那就把剛才提到的http::Cookies結構實體的getaddremove方法都加上_private這個後綴就行了。

另外,加密所使用的密鑰,在預設情況下是由Rocket自動產生,不過這部份其實是可以手動設定的(在正式部署的環境中,如果有用到基於Cookie的會話,一定要手動替每個Rocket應用程式的執行個體設定相同的密鑰),會在之後的章節做介紹。

還有一點要注意的是,在request::FromRequest特性的from_request這個關聯函數中,也是可以透過request::Request結構實體來操作http::Cookies。但是,Rocket限制http::Cookies在同一個時間只能夠有一個實體,因此如果要在同一個路由處理程序的函數中,使用有用到http::Cookies結構體的請求守衛,同時又把http::Cookies直接當請求守衛來使用的話,http::Cookies的順序必須要在這些請求守衛的後面,否則它們得到的http::Cookies結構實體將無實質作用。

例如:

use rocket::Outcome;
use rocket::request::{Outcome as RequestOutcome, Request, FromRequest};
    
struct Custom {
    name: String
}
    
impl<'a, 'r> FromRequest<'a, 'r> for Custom {
    type Error = ();
    
    fn from_request(request: &'a Request<'r>) -> RequestOutcome<Self, Self::Error> {
        let cookies = request.cookies();
    
        let name = cookies.get("name").map(|c| c.value()).unwrap_or("").to_string();

        Outcome::Success(Custom {
            name
        })
    }
}

use rocket::http::Cookies;

#[get("/cookie/message")]
fn handler(custom: Custom, cookies: Cookies) -> String {
    let message = cookies.get("message").map(|c| c.value()).unwrap_or("");

    format!("{}: {}", custom.name, message)
}

主體

HTTP請求中的HTTP主體大致可分為以下三種類型:

  • application/x-www-form-urlencoded:網頁表單預設會用這種。
  • multipart/form-data:網頁表單如果需要上傳檔案資料的話,就會用這種。
  • 其它

Rocket框架本身只能支援application/x-www-form-urlencoded類型的主體,如果要處理不是application/x-www-form-urlencoded類型的主體,只能由開發者自行去處理主體中的原始資料(raw data),或是使用第三方的套件來處理,這個部份由於日後的變動性很大,在本系列文章中就不提了,有興趣者可以參考本站Rust分類中相關的單篇文章。

資料類型為application/x-www-form-urlencoded的主體,可以直接將有實作request::FromFormValue特性的型別包裹成request::Form<T>或是request::LenientForm<T>來匹配。因為request::Form<T>request::LenientForm<T>除了有實作request::FromQuery特性,可當作查詢守衛來用之外,它們也有實作data::FromData特性,可以當作「資料守衛」(Data Guard)來用。

資料守衛用的法與參數守衛、區段守衛和查詢守衛不同,一個路由處理程序最多只能夠使用一個資料守衛,且加入資料守衛的方式為,在HTTP請求方法的屬性參數中,於路徑後面,再多加一個data參數,來指定資料守衛的變數名稱,寫法如下:

#[HTTP請求方法("路徑", data = "<資料守衛的變數名稱>")]
fn handler(資料守衛的變數名稱: 資料守衛的型別) -> 要回應的資料型別 { 處理請求的程式區塊 }

如以下程式:

use rocket::request::Form;

#[derive(FromForm)]
struct User {
    id: i32,
    name: String,
}

#[get("/?hello", data = "<user>")]
fn index(user: Form<User>) -> String {
    format!("Hello, world! id = {}, name = {}", user.id, user.name)
}

以上這個路由處理程序,可以讓Rocket在處理路徑為/的GET請求時,去檢查其網址查詢字串中有無hello欄位,且HTTP主體中的資料類型必須為application/x-www-form-urlencoded,主體的資料中必須只能有idname欄位。此外,id欄位的值必須要可以轉成i32型別,name欄位的值必須要可以轉成String型別。

可省略或是允許出錯的資料欄位

上面介紹的參數守衛、區段守衛和實作了request::FromFormValue特性的型別都可以使用Option或是Result來進行包裹,前者可以使資料欄位變成可選的(optional),後者則可以允許當該資料欄位存在但資料匹配或是轉換失敗時的情形。

例如以下程式:

#[get("/add?<a>&<b>")]
fn handler(a: Option<i32>, b: Option<i32>) -> String {
    format!("Answer = {}", a.unwrap_or(0) + b.unwrap_or(0))
}

以上這個路由處理程序,可以讓Rocket在處理路徑為/add的GET請求時,去檢查其網址查詢字串中有無欄位a或欄位b,如果沒有的話,會直接把該欄位當作是None;如果有的話,該欄位的值必須要可以被轉成i32型別,然後使用Some這個Option的變體來包裹轉換後的i32數值。

再例如以下程式:

use rocket::http::RawStr;

#[get("/add?<a>&<b>")]
fn handler(a: Result<i32, &RawStr>, b: Result<i32, &RawStr>) -> String {
    format!("Answer = {}", a.unwrap_or(0) + b.unwrap_or(0))
}

以上這個路由處理程序,可以讓Rocket在處理路徑為/add的GET請求時,去檢查其網址查詢字串中有無欄位a和欄位b。此兩個欄位都必須存在,若值可以被轉成i32型別,就會使用Ok這個Result的變體來包裹轉換後的i32數值;若值不能轉成i32型別,就會使用Err這個Result的變體來包裹原始的字串資料。

這邊要注意的是,ResultErr所包裹的資料型別是根據使用的參數守衛、區段守衛和實作了request::FromFormValue特性的型別而定,並不一定都是rocket這個crate下的&http::RawStr

當然,我們也可以把OptionResult結合在一起使用,就可以同時使資料欄位變成可選的,而且也允許它轉換失敗啦!

例如以下程式:

use rocket::http::RawStr;

#[get("/add?<a>&<b>")]
fn handler(a: Option<Result<i32, &RawStr>>, b: Option<Result<i32, &RawStr>>) -> String {
    format!("Answer = {}", a.unwrap_or(Ok(0)).unwrap_or(0) + b.unwrap_or(Ok(0)).unwrap_or(0))
}

允許出錯的請求守衛

請求守衛也可以使用OptionResult來進行包裹。若request::FromRequest特性的from_request關聯函數是回傳OutcomeForward變體的話,那麼在經過匹配與轉換之後,Option的變體就會是None。若request::FromRequest特性的from_request關聯函數是回傳OutcomeFailure變體的話,那麼在經過匹配與轉換之後,Option的變體就會是None,而Result的變體就會是Err,且Err變體所包裹的值為Failure變體所包裹的數組(tuple)的第二個值(第一個值是http::Status列舉實體)。

這邊要注意的是,若request::FromRequest特性的from_request關聯函數是回傳OutcomeForward變體,則它無法用加上Result包裹後的請求守衛來進行匹配!

並且,若request::FromRequest特性的from_request關聯函數是回傳OutcomeFailure變體,則當該型別作為請求守衛來使用時,如果請求守衛沒有使用Option或是Result來包裹,就無法使用下一個路由處理程序繼續匹配,而會直接以Failure變體中的http::Status列舉實體來觸發Rocket的錯誤捕獲者(Error Catcher)機制。這個部份會在之後的章節進行更詳細的說明。

總結

在這個章節中,我們知道了如何替Rocket框架的路由處理程序撰寫包含網址查詢字串的路徑匹配樣本,也了解了路由處理程序的預設優先順序和手動設定優先順序的方式,然後也利用了參數守衛、區段守衛、查詢守衛、請求守衛以及資料守衛來取得HTTP請求中含有的資料。

在下一個章節中,我們要來真正地開始學習HTTP回應(response)方式,而不再只是使用String或是&str作為路由處理程序函數的回傳型別。

下一章:HTTP回應(Response)