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



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

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

struct MyFairing;

#[rocket::async_trait]
impl Fairing for MyFairing {
    fn info(&self) -> Info {
        Info {
            name: "the name of this fairing",
            kind: Kind::Ignite | Kind::Liftoff | Kind::Request | Kind::Response | Kind::Shutdown,
        }
    }

    async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
        info!("on_ignite");

        Ok(rocket)
    }

    async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
        info!("on_liftoff");
    }

    async fn on_request(&self, request: &mut Request<'_>, data: &mut Data<'_>) {
        info!("on_request");
    }

    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
        info!("on_response");
    }

    async fn on_shutdown(&self, rocket: &Rocket<Orbit>) {
        info!("on_shutdown");
    }
}

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

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

  • Ignite:當Rocket正要啟動前。也就是使用Rocket結構實體的ignite或是launch方法時。
  • Liftoff:當Rocket啟動後。也就是使用Rocket結構實體的launch方法後。
  • Request:當接收到HTTP請求,在尋找可匹配的路由處理程序前時。
  • Response:當路由處理程序回傳HTTP回應後,在其被傳送給客戶端前時。
  • Shutdown:當Rocket正在被優雅的關機時。

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

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

struct MyFairing;

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

    async fn on_request(&self, request: &mut Request<'_>, data: &mut Data<'_>) {
        info!("on_request");
    }

    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
        info!("on_response");
    }
}

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

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

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

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

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

#[rocket::async_trait]
impl Fairing for MethodsCounter {
    fn info(&self) -> Info {
        Info {
            name: "The Methods Counter", kind: Kind::Request | Kind::Response
        }
    }

    async fn on_request(&self, request: &mut Request<'_>, _data: &mut 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),
        };
    }

    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
        if response.status() == Status::NotFound
            && 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: {get_count}\nPost: {post_count}\nOthers: {others_count}");

            response.set_status(Status::Ok);
            response.set_header(ContentType::Plain);
            response.set_sized_body(body.len(), 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方法,將要註冊的整流片實體當作參數傳進去即可。如下:

rocket::build().attach(MethodsCounter::default())

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

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

例如:

use rocket::{fairing::AdHoc, http::Method};

...

rocket::build()
    .attach(AdHoc::on_ignite("Launch Printer", |rocket| {
        Box::pin(async move {
            info!("Rocket is about to launch! Exciting! Here we go...");

            rocket
        })
    }))
    .attach(AdHoc::on_request("Always-Get Rewriter", |req, _| {
        Box::pin(async move {
            req.set_method(Method::Get);
        })
    }))

...

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

#[macro_use]
extern crate rocket;

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

use rocket::{
    fairing::AdHoc,
    http::{ContentType, Method, Status},
    State,
};

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

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(MethodsCounter::default())
        .attach(AdHoc::on_request("Request Methods Counter", |req, _| {
            Box::pin(async move {
                let methods_counter = req.guard::<&State<MethodsCounter>>().await.unwrap();

                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| {
            Box::pin(async move {
                if res.status() == Status::NotFound
                    && req.method() == Method::Get
                    && req.uri().path() == "/counts"
                {
                    let methods_counter = req.guard::<&State<MethodsCounter>>().await.unwrap();

                    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: {get_count}\nPost: {post_count}\nOthers: {others_count}");

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

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

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

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

use std::{convert::TryFrom, str::FromStr};

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))
        }
    }
}

impl FromStr for BufferSize {
    type Err = &'static str;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let value: i64 = value.parse().map_err(|_| "the buffer size needs to be an integer")?;

        Self::try_from(value)
    }
}

...

use rocket::fairing::AdHoc;

rocket::build().attach(AdHoc::try_on_ignite("BUFFER_SIZE Checker", |rocket| {
    Box::pin(async move {
        let buffer_size = rocket.figment().find_value("buffer_size");

        let checked_buffer_size = match buffer_size {
            Ok(buffer_size) => match buffer_size.as_str() {
                Some(buffer_size) => match BufferSize::from_str(buffer_size) {
                    Ok(buffer_size) => buffer_size,
                    Err(error) => {
                        error!("{error}");

                        return Err(rocket);
                    },
                },
                None => {
                    error!("the buffer size needs to be an integer");

                    return Err(rocket);
                },
            },
            Err(_) => BufferSize::default(),
        };

        Ok(rocket.manage(checked_buffer_size))
    })
}))

...

如此一來,檢查過後的buffer_size就會被儲存成BufferSize實體,被註冊進Rocket的應用程式狀態中。注意這邊使用到了rocket結構實體提供的try_on_ignite方法,而不是on_ignite,為的是讓BUFFER_SIZE Checker整流片有能力在發現問題時中止Rocket應用程式的啟動。

總結

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

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

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