ETag是HTTP提供的快取機制,可以讓伺服器利用簡單的字串比對,來驗證客戶端先前快取到的HTTP回應是否依然有效。它的流程主要是這樣:伺服器在回應資料的同時,於HTTP回應標頭中夾帶ETag欄位,這個欄位的值就相當於這個資料的ID,而客戶端在將HTTP回應快取下來之後,若需重新發送請求時,就會在HTTP請求中夾帶If-None-Match欄位,將原先快取到的ETag值再回傳給伺服器,讓伺服器來判斷客戶端的快取是否需要更新。如果不需要,伺服器就會回傳304(Not Modified)這個HTTP狀態碼,讓客戶端繼續使用快取的資料;如果需要讓客戶端更新快取的話,就要再傳送一次完成的HTTP回應,並在HTTP標頭中夾帶新的ETag欄位值。



Etag If-None-Match Request Guard for Rocket Framework

「Etag If-None-Match Request Guard for Rocket Framework」是筆者開發的套件,提供了一個請求守衛,可以用來讀取HTTP請求中的If-None-Match欄位。

Crates.io

Cargo.toml

rocket-etag-if-none-match = "*"

使用方法

rocket_etag_if_none_match這個crate提供的EtagIfNoneMatch結構體可以當作請求守衛,可以用來讀取HTTP請求中的If-None-Match欄位。EtagIfNoneMatch結構實體提供了weak_eqstrong_eq方法,可以用來驗證從參數傳入的rocket::http::hyper::header::EntityTag是否在邏輯上相等於If-None-Match欄位的ETag值。

一個弱的(weak)ETag值長得像以下這樣:

W/"123456789"

一個強的(strong)ETag值長得像以下這樣:

"123456789"

weak_eq是用來判斷兩筆帶有ETag值的資料是否為邏輯上的相等(例如JSON格式的字串,有筆是{"a": 1, "b": 2},另一筆是{"b": 2, "a": 1},這兩筆資料可以說在邏輯上是相等的),而strong_eq是用來判斷資料是否完全一致,不去管資料的格式是什麼。如果A資料的ETag值strong_eq於B資料的ETag值,則它們也必定能weak_eq。被用來做strong_eq判斷的ETag值只能是強的(strong)ETag值。

以下是一個ETag機制的實作範例:

#[macro_use]
extern crate rocket;

use std::borrow::Cow;
use std::io::Cursor;

use rocket_etag_if_none_match::{entity_tag::EntityTag, EtagIfNoneMatch};

use rocket::http::Status;
use rocket::request::Request;
use rocket::response::{Responder, Response, Result};

use chrono::prelude::*;

static MY_ETAG: EntityTag = unsafe { EntityTag::new_unchecked(true, Cow::Borrowed("MAGIC")) };

struct MyResponse<'r>(Response<'r>);

impl<'r, 'o: 'r> Responder<'r, 'o> for MyResponse<'o> {
    #[inline]
    fn respond_to(self, _: &'r Request<'_>) -> Result<'o> {
        Ok(self.0)
    }
}

impl<'r> From<Response<'r>> for MyResponse<'r> {
    #[inline]
    fn from(res: Response<'r>) -> Self {
        MyResponse(res)
    }
}

#[get("/")]
fn index(etag_if_none_match: EtagIfNoneMatch) -> MyResponse<'static> {
    if etag_if_none_match.weak_eq(&MY_ETAG) {
        println!("Cached!");
        Response::build().status(Status::NotModified).finalize().into()
    } else {
        let body = format!("Current Time: {}\n\nTry to re-open this page repeatedly without pressing the forced-refresh(Ctrl+F5) button.", Utc::now().to_rfc3339());

        let size = body.len();

        Response::build()
            .raw_header("Etag", MY_ETAG.to_string())
            .raw_header("Content-Type", "text/plain; charset=utf-8")
            .sized_body(size, Cursor::new(body))
            .finalize()
            .into()
    }
}

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

MY_ETAG是我們寫死在程式中的ETag,它是一個弱的(weak)ETag值,以完整的字串來表示的話為W/"MAGIC"。當路由處理程序index被呼叫時,會去檢查HTTP請求中的If-None-Match欄位是否weak_eqW/"MAGIC",如果可以的話就直接回傳304(Not Modified)這個HTTP狀態碼;如果不行的話就回傳完整的HTTP回應。

善用HTTP的ETag機制和先前介紹過的Cache-Control機制,除了可以大幅度地減少伺服器的負擔外,還可以減短使用者瀏覽網頁時的等待時間。