大多數的Web框架都會提供中介軟體(Middleware)機制,使用固定的額外程式來處理每次的HTTP請求或是HTTP回應。Rocket框架對於中介軟體的支援是依靠「整流片」(Fairing)來完成的,我們可以替任意的型別實作fairing::Fairing特性,使其能作為「掛鉤」(hook)來監聽Rocket框架收到HTTP請求時的事件,或是要送出HTTP回應時的事件。



fairing::Fairing特性的基本實作方式如下:

use rocket::Rocket;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::request::Request;
use rocket::response::Response;
use rocket::data::Data;

struct MyFairing;

impl Fairing for MyFairing {
    fn info(&self) -> Info {
        Info {
            name: "the name of this fairing",
            kind: Kind::Attach | Kind::Launch | Kind::Request | Kind::Response
        }
    }

    fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
        println!("on_attach");

        Ok(rocket)
    }

    fn on_launch(&self, _rocket: &Rocket) {
        println!("on_launch");
    }

    fn on_request(&self, _request: &mut Request, _data: &Data) {
        println!("on_request");
    }

    fn on_response(&self, _request: &Request, _response: &mut Response) {
        println!("on_response");
    }
}

info這個由fairing::Fairing特性提供的方法中,我們必須要回傳一個fairing::Info結構實體,裡面有現在正在被實作的整流片的名稱,以及它所要監聽的事件,事件會以fairing::Kind結構實體來表示。fairing::Kind結構體有實作BitOr特性,因此可以直接使用|進行OR運算,來合併多種fairing::Kind結構實體,以監聽多種不同的事件。

一個整流片可以監聽的事件有以下四種:

  • Attach:當這個結構實體被註冊到某個Rocket結構實體時。也就是使用Rocket結構實體的attach方法時。
  • Launch:當Rocket正要啟動前。也就是使用Rocket結構實體的launch方法時。
  • Request:當接收到HTTP請求,在尋找可匹配的路由處理程序前時。
  • Response:當路由處理程序回傳HTTP回應後,在其被傳送給客戶端前時。

每種事件在被觸發的時候會去執行fairing::Fairing特性中對應的方法,在實作fairing::Fairing特性時,只需要針對有被監聽的事件來實作方法即可,不需要將所有事件的對應方法都實作出來(因為這些方法已有預設的實作)。例如,只想監聽Request和Response事件的話,程式可以改寫如下:

use rocket::Rocket;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::request::Request;
use rocket::response::Response;
use rocket::data::Data;

struct MyFairing;

impl Fairing for MyFairing {
    fn info(&self) -> Info {
        Info {
            name: "the name of this fairing",
            kind: Kind::Request | Kind::Response
        }
    }

    fn on_request(&self, _request: &mut Request, _data: &Data) {
        println!("on_request");
    }

    fn on_response(&self, _request: &Request, _response: &mut Response) {
        println!("on_response");
    }
}

另外,data::Data這個結構體我們以前並未提到,它其實也是一個資料守衛,可以用來匹配任何格式的HTTP主體,能夠直接讀取HTTP主體中的原始資料。

舉個例子,實作出一個可以分別統計GET和POST請求次數的整流片。如下:

use std::sync::atomic::{Ordering, AtomicUsize};
use std::io::Cursor;

use rocket::fairing::{Fairing, Info, Kind};
use rocket::request::Request;
use rocket::response::Response;
use rocket::data::Data;
use rocket::http::{Method, Status, ContentType};

#[derive(Default)]
struct MethodsCounter {
    get: AtomicUsize,
    post: AtomicUsize,
    others: AtomicUsize,
}

impl Fairing for MethodsCounter {
    fn info(&self) -> Info {
        Info {
            name: "the name of this fairing",
            kind: Kind::Request | Kind::Response
        }
    }

    fn on_request(&self, request: &mut Request, _data: &Data) {
        match request.method() {
            Method::Get => self.get.fetch_add(1, Ordering::Relaxed),
            Method::Post => self.post.fetch_add(1, Ordering::Relaxed),
            _ => self.others.fetch_add(1, Ordering::Relaxed)
        };
    }

    fn on_response(&self, request: &Request, response: &mut Response) {
        if response.status() == Status::NotFound {
            if request.method() == Method::Get && request.uri().path() == "/counts" {
                let get_count = self.get.load(Ordering::Relaxed);
                let post_count = self.post.load(Ordering::Relaxed);
                let others_count = self.others.load(Ordering::Relaxed);
                let body = format!("Get: {}\nPost: {}\nOthers: {}", get_count, post_count, others_count);

                response.set_status(Status::Ok);
                response.set_header(ContentType::Plain);
                response.set_sized_body(Cursor::new(body));
            }
        }
    }
}

以上程式,我們在fairing::Fairing特性的on_request方法中,去判斷HTTP請求方法的類型,並且使MethodsCounter結構實體的對應欄位值加1。on_response方法中,我們攔截了HTTP狀態碼為404(NotFound)的所有回應,並判斷其HTTP請求方法是否為GET,且路徑為/counts。如果是的話,就將回應的HTTP狀態碼修改為200(Ok),Content-Type標頭欄位修改為text/plain; charset=utf-8,主體修改為MethodsCounter結構實體的所有欄位資訊(也就是當前的請求次數的統計結果)。

若要將MethodsCounter整流片註冊給Rocket來使用的話,就使用Rocket結構實體提供的attach方法,將要註冊的整流片實體當作參數傳進去即可。如下:

...

let rocket = rocket::ignite();

let rocket = rocket.attach(MethodsCounter::default());

...

直接用閉包當作整流片掛鉤

如果覺得實作fairing::Fairing特性很麻煩的話,也可以直接透過fairing::AdHoc結構體來包裹某個閉包,將其變成整流片來使用。這個方式適用於簡單的處理,無法如上面那個例子一樣,利用結構體來直接記錄狀態。

例如:

use rocket::fairing::AdHoc;

...

let rocket = rocket::ignite();

let rocket = rocket.attach(AdHoc::on_launch("Launch Printer", |_| {
        println!("Rocket is about to launch! Exciting! Here we go...");
    }))
    .attach(AdHoc::on_request("Always-Get Rewriter", |req, _| {
        req.set_method(Method::Get);
    }));

...

不過,我們也可以在整流片中透過萬用的request::Request結構實體來存取Rocket的「應用程式狀態」,例如:

use rocket::fairing::AdHoc;
use rocket::State;

...

let rocket = rocket::ignite();

let rocket = rocket.manage(MethodsCounter::default());

let rocket = rocket.attach(AdHoc::on_request("Request Methods Counter", |req, _| {
        let methods_counter = req.guard::<State<MethodsCounter>>().unwrap().inner();

        match req.method() {
            Method::Get => methods_counter.get.fetch_add(1, Ordering::Relaxed),
            Method::Post => methods_counter.post.fetch_add(1, Ordering::Relaxed),
            _ => methods_counter.others.fetch_add(1, Ordering::Relaxed)
        };
    }))
    .attach(AdHoc::on_response("Request Methods Getter", |req, res| {
        if res.status() == Status::NotFound {
            if req.method() == Method::Get && req.uri().path() == "/counts" {
                let methods_counter = req.guard::<State<MethodsCounter>>().unwrap().inner();

                let get_count = methods_counter.get.load(Ordering::Relaxed);
                let post_count = methods_counter.post.load(Ordering::Relaxed);
                let others_count = methods_counter.others.load(Ordering::Relaxed);
                let body = format!("Get: {}\nPost: {}\nOthers: {}", get_count, post_count, others_count);

                res.set_status(Status::Ok);
                res.set_header(ContentType::Plain);
                res.set_sized_body(Cursor::new(body));
            }
        }
    }));

...

用整流片處理Rocket框架的設定值

在先前的章節中有提到Rocket框架的設定檔可以傳入我們自訂的設定項目,由於Rocket的主旨在於安全的型別化,因此所有從外面傳進來的設定值都必須要在Rocket框架啟動前,經過程式檢查過格式後才可以被拿來使用。而檢查設定值最好的時機點,就是在整流片被觸發Attach事件時啦!

舉例來說,我們有個自訂的設定項目名稱buffer_size,可以用來設定堆疊空間中緩衝空間的大小。我們希望這個值必須要大於等於256,且小於等於67108864。利用剛才介紹的fairing::AdHoc結構體,可以撰寫出如以下的檢查程式:

use std::convert::TryFrom;

const DEFAULT_BUFFER_SIZE: usize = 4096;

pub struct BufferSize(usize);

impl BufferSize {
    pub fn get_value(&self) -> usize {
        self.0
    }
}

impl Default for BufferSize {
    fn default() -> Self {
        BufferSize(DEFAULT_BUFFER_SIZE)
    }
}

impl TryFrom<i64> for BufferSize {
    type Error = &'static str;

    fn try_from(value: i64) -> Result<Self, Self::Error> {
        if value < 256 {
            Err("the buffer size is smaller than 256")
        } else if value > 67108864 {
            Err("the buffer size is bigger than 67108864")
        } else {
            Ok(BufferSize(value as usize))
        }
    }
}

...

use rocket::fairing::AdHoc;

let rocket = rocket.attach(AdHoc::on_attach("BUFFER_SIZE Checker", |rocket| {
        let buffer_size = rocket.config().extras.get("buffer_size");
    
        let checked_buffer_size = match buffer_size {
            Some(buffer_size) => {
                let buffer_size = buffer_size.as_integer().expect("the buffer size needs to be an integer");
    
                BufferSize::try_from(buffer_size).unwrap()
            }
            None => {
                BufferSize::default()
            }
        };
    
        Ok(rocket.manage(checked_buffer_size))
    }));

...

如此一來,檢查過後的buffer_size就會被儲存成BufferSize實體,被註冊進Rocket的應用程式狀態中。

總結

Rocket框架提供的「整流片」機制即類似其它Web框架所提供的「中介軟體」機制。由於整流片在監聽HTTP請求和回應事件時,有很大的可能會需要直接操作request::Request結構實體和response::Response結構實體,算是比較低階的用法,所以在程式實作上會稍微麻煩了一點,對於程式效能的影響也是需要加以考量的。

在下一個章節中,將會回頭介紹更進階的處理HTTP回應的方式。

下一章:進階處理HTTP回應(Response)的方式