到目前為止,我們已經知道一個路由處理程序的函數可以選擇不回傳任何值,或是選擇回傳一個字串或一個字串切片。在這篇文章中,將會介紹Rocket框架所提供的多種不同的型別,來讓我們的路由處理程序能夠回應(respond)更多類型的資料。



response::Responder

先來複習一下上一章所提到的Rocket的路由處理程序的基本架構,如下:

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

在這個架構中,要回應的資料型別可以使用任何實作了rocket這個crate提供的response::Responder特性的型別。Rocket框架預設已經替&str(字串切片)、String(字串)、&[u8](u8陣列切片)、Vec<u8>File等Rust與標準函式庫內建的型別實作了response::Responder特性。另外,Rocket還在response模組中另外內建了ContentNamedFileRedirectStream型別,來處理常用的回應資料類型。response::status模組和http::Status列舉則可以用來直接回傳HTTP的狀態碼。

由於HTTP回應並不只是把資料放進HTTP主體中,我們總是還需要針對資料類型、資料大小以及資料的快取來撰寫HTTP回應的標頭。而Rocket框架顯然是把HTTP標頭的寫入動作都交由response::Responder特性來解決了,我們無法直接從路由處理程序的架構得知它回應的HTTP標頭到底會有哪些。所以說,在使用一個陌生的response::Responder前,最好先熟悉一下該型別會如何處理HTTP回應中的標頭。

不回傳或是回傳()

在Rust程式語言中,不回傳資料的函數其實就是回傳()這個型別的值。如果我們的路徑處理程序不想要在HTTP回應中回傳任何多餘的資料,可以替路徑處理程序定義不需回傳資料或是回傳值型別為()的函數。

例如:

#[delete("/resources")]
fn handler() {
    println!("Resources are deleted!");
}

或:

#[delete("/resources")]
fn handler() -> () {
    println!("Resources are deleted!");
}

如此一來,HTTP回應中的標頭欄位只會有基本的三項:

  • Server:Rocket框架預設會自動在每個HTTP回應中添加這個欄位,並且將值設定為Rocket。如果想要改寫這個欄位,可以與如Nginx等前端的網頁伺服器搭配使用。
  • Content-Length:一個HTTP回應中通常會有這個欄位。這個欄位的值為HTTP主體的資料大小,單位為位元組(bytes)。如果HTTP主體沒有資料,這個欄位的值應被設為0
  • Date:這個欄位的值為此HTTP回應在伺服器上的建立時間,值的格式可參考RFC 7231

回傳&'static str&str

當一個函數回傳值的型別為參考時,還記得Rust學習之路系列文章中的泛型、特性和生命周期中介紹的編譯器推論函數的參數和回傳值的生命周期的規則嗎?若要回傳生命周期為'static的字串切片,記得要在回傳值型別中明確地把'static寫出來。

例如:

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

而如果是要回傳可以被編譯器直接推論出生命周期的參考,則函數的回傳值型別可以省略不寫生命周期。

例如:

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
}

當路由處理程序的函數是回傳字串切片時,HTTP回應中的標頭欄位除了上面提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會被設定為text/plain; charset=utf-8,也就是以UTF-8編碼的純文字資料。所以,使用字串切片是無法正常回傳HTML網頁的哦!

回傳String

例如:

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

當路由處理程序的函數是回傳字串時,HTTP回應中的標頭欄位除了上面提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會被設定為text/plain; charset=utf-8,也就是以UTF-8編碼的純文字資料。所以,使用字串也是無法正常回傳HTML網頁的哦!

回傳&'static [u8]&[u8]

因為生命周期限制的緣故,路由處理程序的函數幾乎不會回傳&[u8],所以這邊就不示例了。不過如果是&'static [u8],還是會有機會用到的。可以搭配Rust程式語言內建的include_bytes巨集,使路由處理程序直接在HTTP回應的主體中提供某個檔案的資料內容,且這個檔案會在程式編譯階段時,也跟著被編進程式執行檔中。

例如:

#[get("/file")]
fn handler() -> &'static [u8] {
    include_bytes!("/path/to/file")
}

如果您對單檔執行有興趣,可以延伸閱讀這篇文章

當路由處理程序的函數是回傳u8陣列切片時,HTTP回應中的標頭欄位除了上面提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會被設定為application/octet-stream,也就是不知道是什麼格式的資料串流啦!通常我們會對u8陣列切片再包一層實作了response::Responder的型別,用來覆寫Content-Type欄位,甚至還會順便添加更多標頭欄位(如快取相關的欄位)。

回傳Vec<u8>

例如:

extern crate rand;

use rand::RngCore;
    
#[get("/salt")]
fn handler() -> Vec<u8> {
    let mut result = vec![0u8; 16];

    rand::thread_rng().fill_bytes(&mut result);

    result
}

當路由處理程序的函數是回傳Vec<u8>時,HTTP回應中的標頭欄位除了上面提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會被設定為application/octet-stream,也就是不知道是什麼格式的資料串流啦!通常我們會對Vec<u8>再包一層實作了response::Responder的型別,用來覆寫Content-Type欄位,甚至還會順便添加更多標頭欄位(如快取相關的欄位)。

回傳File

例如:

use std::fs::File;
    
#[get("/file")]
fn handler() -> File {
    File::open("/path/to/file").unwrap()
}

當路由處理程序的函數是回傳File時,HTTP回應中的標頭欄位只會有上面提到的三個基本欄位。

回傳response::content::*

當我們想要控制回傳的HTTP回應的Content-Type標頭欄位時,可以使用response::content模組下的結構體來包裹要回傳的資料。

結構體與Content-Type欄位值的對應關係如下表:

結構體名稱 Content-Type欄位值
Css text/css
Html text/html
JavaScript application/javascript
Json application/json
MsgPack application/msgpack
Plain text/plain
Xml text/xml

例如:

use rocket::response::content::Html;
    
#[get("/")]
fn handler() -> Html<&'static str> {
    Html(
r#"<html>
    <head>
        <meta charset=UTF-8>
        <title>My Website</title>
    </head>
    <body>
        Welcome!
    </body>
</html>"#
    )
}

如果要使用上表沒有的Content-Type欄位值,可以使用response::Content來包裹要使用的Content-Type欄位值所對應的http::ContentType實體,與要回傳的資料。

例如:

use rocket::response::Content;
use rocket::http::ContentType;

#[get("/file")]
fn handler() -> Content<&'static [u8]> {
    Content(ContentType::PDF, include_bytes!("/path/to/file"))
}

http::ContentType已經內建了許多常用副檔名的MIME類型。如果想要自行填入MIME類型的話,程式可以這樣寫:

use rocket::response::Content;
use rocket::http::ContentType;

#[get("/file")]
fn handler() -> Content<&'static [u8]> {
    Content(ContentType::new("application", "x-person"), include_bytes!("/path/to/file"))
}

另外,http::ContentType的參考型別,也就是&http::ContentType,也可以當作請求守衛來用哦!

回傳response::NamedFile

如果我們想要讓Rocket提供靜態檔案(如圖片檔、CSS、JS等等的靜態資源檔),可以讓路由處理程序的函數回傳response::NamedFile

例如:

use rocket::response::NamedFile;
    
#[get("/file")]
fn handler() -> NamedFile {
    NamedFile::open("/path/to/file").unwrap()
}

當路由處理程序的函數是回傳response::NamedFile時,HTTP回應中的標頭欄位除了上面提到的三個基本欄位外,還會自動加入Content-Type欄位。這個Content-Type欄位的值會根據response::NamedFile開啟的檔案類型(依照檔案副檔名)而自動作適當的變化。

如果想要提供檔案系統中某個目錄下的所有檔案,可以讓response::NamedFile與區段守衛搭配使用。

例如:

use std::path::{Path, PathBuf};
use rocket::response::NamedFile;
    
#[get("/file/<path..>")]
fn handler(path: PathBuf) -> NamedFile {
    NamedFile::open(Path::join(Path::new("static/"), path)).unwrap()
}

不過,對於這方面的功能,Rocket框架還有提供一種更好用的作法,會在以後的章節來介紹。

回傳response::Redirect

response::Redirect結構體可以實現客戶端的轉址(redirection)功能。它提供了以下幾個關聯函數:

  • moved:HTTP回應的狀態碼為301,屬於永久性的轉址。有些網頁瀏覽器或是HTTP客戶端對於POST、PUT或是DELETE等帶有HTTP主體的請求方法,可能會無法自動再使用相同請求方法來重新傳送HTTP主體,所以Rokcet框架比較建議使用permanent關聯函數。
  • found:HTTP回應的狀態碼為302,屬於暫時性的轉址。有些網頁瀏覽器或是HTTP客戶端對於POST、PUT或是DELETE等帶有HTTP主體的請求方法,可能會無法自動再使用相同請求方法來重新傳送HTTP主體,所以Rokcet框架比較建議使用temporary或是to關聯函數。
  • to:HTTP回應的狀態碼為303,屬於暫時性的轉址。永遠使用GET請求方法來進行轉址。
  • temporary:HTTP回應的狀態碼為307,屬於暫時性的轉址。永遠使用與前次相同的HTTP請求方法來進行轉址,可以正常重新傳送HTTP主體。
  • permanent:HTTP回應的狀態碼為308,屬於永久性的轉址。永遠使用與前次相同的HTTP請求方法來進行轉址,可以正常重新傳送HTTP主體。

這些關聯方法可以傳入一個可作為Uri使用的型別的值。雖然直接使用字串或是字串切片也是可以的,例如:

use rocket::response::Redirect;
    
#[get("/old?<id>")]
fn handler(id: i32) -> Redirect {
    Redirect::temporary(format!("/new?id={}&from_old=true", id))
}

#[get("/new?<id>&<from_old>")]
fn handler_new(id: i32, from_old: Option<bool>) -> String {
    let mut s = if from_old.unwrap_or(false) {
        String::from("Hi, old friend. ")
    } else {
        String::new()
    };

    use std::fmt::Write;

    s.write_fmt(format_args!("Your id is {}.", id)).unwrap();

    s
}

但是Rocket既然號稱要做到「型別安全」,網址的組成當然也要是安全的才行。所以Rocket框架還提供了一個有用的巨集──uri

我們可以將以上程式用uri巨集改寫如下:

use rocket::response::Redirect;
    
#[get("/old?<id>")]
fn handler(id: i32) -> Redirect {
    Redirect::temporary(uri!(handler_new: id = id, from_old = true))
}

#[get("/new?<id>&<from_old>")]
fn handler_new(id: i32, from_old: Option<bool>) -> String {
    let mut s = if from_old.unwrap_or(false) {
        String::from("Hi, old friend. ")
    } else {
        String::new()
    };

    use std::fmt::Write;

    s.write_fmt(format_args!("Your id is {}.", id)).unwrap();

    s
}

uri巨集可以在程式編譯階段,去檢查要串接的Uri是否符合指定的路由處理程序所定義的匹配方式。如此一來就不用擔心日後修改路由處理程序的API時,會忘記把原本連到舊API的網址也一併改過來而造成程式在執行階段出錯了!另外,網址查詢字串中,有些欄位是可以省略的,如果要在uri巨集中省略某個可省略的網址查詢字串的欄位,那就把該欄位的值設定為_吧!如下:

uri!(handler_new: id = id, from_old = _)

uri巨集固然好用,只是比較可惜的是,uri巨集只適用於串接出來的Uri是連到目前這個Rocket應用程式本身所能提供路由服務的某個路徑(換句話說就是目前使用Rocket框架的程式專案中,已存在的路由處理程序)。如果是要連到其它的Web應用程式,或是外部網站的話,就不能使用uri巨集了。

如果查詢守衛是使用request::Form<T>或是request::LenientForm<T>,可以在替我們的型別加上FromForm這一個derive屬性的參數時,也順便加上UriDisplayQuery參數。如此一來這個型別也能夠直接使用在uri巨集中。

例如:

#[derive(FromForm, UriDisplayQuery)]
struct User {
    id: i32,
    name: Option<String>,
    email: Option<String>
}

#[get("/old?<user..>")]
fn handler(user: Form<User>) -> Redirect {
    Redirect::temporary(uri!(handler_new: from_old = true, user = user.into_inner()))
}

#[get("/new?<from_old>&<user..>")]
fn handler_new(from_old: Option<bool>, user: Form<User>) -> String {
    let mut s = if from_old.unwrap_or(false) {
        String::from("Hi, old friend. ")
    } else {
        String::new()
    };

    use std::fmt::Write;

    s.write_fmt(format_args!("Your id is {}.", user.id)).unwrap();

    s
}

此外,如果路由處理程序的註冊路徑不是/的話,可以在uri巨集的第一個參數中,插入指定的註冊路徑,例如:

uri!("/path", handler_new: id = id, from_old = _)

回傳response::Stream

若我們想要把某個有實作Read特性的型別的實體,直接當作HTTP回應主體的資料來源的話,可以透過response::Stream來進行包裹,並且設定每個組塊(chunk)的大小。

例如:

use std::fs::File;
use rocket::response::Stream;
    
#[get("/file")]
fn handler() -> Stream<File> {
    Stream::chunked(File::open("/path/to/file").unwrap(), 4096)
}

當路由處理程序的函數是回傳response::Stream時,HTTP回應中的標頭欄位中的三個基本欄位,會缺少Content-Length這一項,因為Read特性只能讓我們從頭到尾讀取某段資料,無法讓我們在讀取前事先得知接下來要讀取的資料總大小。此外,標頭欄位中還會多出一個Transfer-Encoding欄位,且欄位的值會是chunked

回傳response::status::*http::Status

通常如果路由處理程序的函數不回傳資料或是有正常回傳資料的話,HTTP回應的狀態碼會使用200。如果要進行更改,可以利用response::status這個模組底下的結構體來包裹原本要回傳的資料。

response::status模組所提供的結構體有以下這些:

  • Created:HTTP回應的狀態碼為201
  • Accepted:HTTP回應的狀態碼為202
  • NoContent:HTTP回應的狀態碼為204
  • BadRequest:HTTP回應的狀態碼為400
  • Unauthorized:HTTP回應的狀態碼為401
  • Forbidden:HTTP回應的狀態碼為403
  • NotFound:HTTP回應的狀態碼為404
  • Conflict:HTTP回應的狀態碼為409
  • Custom:HTTP回應的狀態碼根據傳入的http::Status列舉的變體來決定。

例如:

use rocket::response::status;
    
#[get("/404")]
fn handler() -> status::NotFound<&'static str> {
    status::NotFound("Sorry, I couldn't find it!")
}

或是:

use rocket::http::Status;
use rocket::response::status;
    
#[get("/404")]
fn handler() -> status::Custom<&'static str> {
    status::Custom(Status::NotFound, "Sorry, I couldn't find it!")
}

如果我們只想利用HTTP回應的狀態碼來提示API的調用結果,可以直接讓路由處理程序的函數回傳http::Status這個列舉的變體。

例如:

use rocket::http::Status;
    
#[get("/404")]
fn handler() -> Status {
    Status::NotFound
}

這邊要注意的是,並不是每個http::Status列舉的變體都可以直接被路由處理程序的函數回傳,有些有著必須與其它資料搭配才有特殊功用的變體(如AcceptedSeeOther等),若直接拿來回傳,就會使Rocket框架自動將這些變體改為InternalServerError

讓路由處理程序的函數回傳http::Status的作法,如果使用的變體是用來表示客戶端或是伺服器端的「錯誤」的話(例如NotFound或是InternalServerError),就會觸發Rocket框架的錯誤捕獲者(Error Catcher)機制,而去使用Rocket在啟動前就先設定好的錯誤捕獲者,或是使用Rocket預設的錯誤處理方式,來協助處理HTTP回應。這部份會在之後的章節做詳細介紹。

response::Response

如果臨時想要使用某種HTTP回應,可以使用response::Response結構體所提供的build關聯函數,直接在路由處理程序中產生response::ResponseBuilder結構實體,來手動設定要回傳的HTTP回應中該放的標頭欄位和主體。最後再使用response::ResponseBuilder結構實體提供的finalize方法,來建立出要回傳的response::Response結構實體。

例如:

use std::io::Cursor;
use rocket::http::{Status, ContentType};
use rocket::response::Response;

extern crate rand;

use rand::RngCore;
    
#[get("/salt")]
fn handler() -> Response<'static> {
    let mut result = vec![0u8; 16];

    rand::thread_rng().fill_bytes(&mut result);

    Response::build()
        .status(Status::Ok)
        .header(ContentType::Binary)
        .raw_header("Content-Disposition", "attachment")
        .sized_body(Cursor::new(result))
        .finalize()
}

response::ResponseBuilder結構實體所提供的sized_body方法,可以用來設定HTTP回應中的主體內的資料。sized_body方法可以傳入一個可以在資料傳送前就能知道其資料總大小的型別(要實作SeekRead特性)。如果無法在資料傳送前就知道其總大小的話,就要使用streamed_body或是chunked_body方法來傳送(該資料的型別只需實作Read特性)。sized_body方法會自動替HTTP回應中的標頭欄位Content-Length設定正確的值;streamed_body或是chunked_body方法則會不會加上Content-Length欄位,而是加上Transfer-Encoding欄位,且欄位值會被設定為chunked

使用Option或是Result來包裹要回傳的型別

路由處理程序的函數所回傳的資料型別,可以使用Option或是Result來進行包裹。

若是使用Option來包裹,當回傳的Option變體為None時,等同於回傳了上面提過的http::Status列舉的NotFound變體。

例如:

use std::path::{Path, PathBuf};
use rocket::response::NamedFile;
    
#[get("/file/<path..>")]
fn handler(path: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::join(Path::new("static/"), path)).ok()
}

若是使用Result來包裹,Ok或是Err變體所包裹的資料型別都必須是有實作response::Responder特性的型別。把Result當作路由處理程序的函數回傳型別的功用,就像是將原先只有一種回傳型別的函數變成兩種(換句話說,原本這支API可以回傳一種HTTP回應,加上Result來包裹後就可以將其擴充為兩種)。

例如:

use std::path::{Path, PathBuf};
use rocket::response::NamedFile;
use rocket::response::status;
    
#[get("/file/<path..>")]
fn handler(path: PathBuf) -> Result<NamedFile, status::NotFound<&'static str>> {
    NamedFile::open(Path::join(Path::new("static/"), path)).map_err(|_| status::NotFound("Cannot find this file."))
}

當然,我們也可以利用多層(巢狀)Result來包裹要拿來回傳的不同型別的資料,實現出能夠回傳多種HTTP回應的路由處理程序。

總結

在這個章節中,我們學會了路由處理程序的函數的回傳值型別的用法。至目前為止,我們已經將一個Web框架所具備的基本功能(路由和回應HTTP請求)都學全了,但是我們還不知道如何配置Rocket應用程式,使其能夠運作在正式的伺服器環境中。所以下一章將會介紹Rocket的組態配置方式。

下一章:設定Rocket框架