🚀

RustのRocketのメモ(リクエスト)

2023/09/25に公開

Rust のRocketのリクエスト関連のメモです。ここを読みながら確認してみたこと等をメモします。

リクエスト

パラメータの型

下記のようにage:u8となっている場合、/bob/32/trueとかだと OK。でも/bob/a/trueとか u8 型を求めているのに文字列とかだと 404 エラーになる。bool はtruefalseのいずれかなら OK。

#[get("/<name>/<age>/<cool>")]
fn hello(name: &str, age: u8, cool: bool) -> String {
  ...
}

同じパス・メソッド

2 つ以上のルートが同じパスとメソッドになっていると、下記エラーが発生する。同じパス・メソッドでも、型を変えたらよいかも?と思ったけど、そういうのはダメらしい。

Error: Rocket failed to launch due to the following route collisions

と思ったら、rankをつけると、同じパス・メソッドでも型違うやつを下記のように複数作れた。rank が小さい程優先度が高くなる。

#[get("/<hoge>")]
fn hoge(hoge: u8) -> String {
    format!("Hoge! {}", hoge)
}

#[get("/<hoge>", rank = 2)]
fn hoge2(hoge: &str) -> String {
    format!("Hoge2! {}", hoge)
}

複数のパスを 1 つの変数で扱う

下記のように..を使うと複数のパスを一つの変数として扱える。下記だとpathは、static/hoge.txtとかになりますが、この場合プロジェクトルートの static を探すらしい。(下記ファイルがある場所ではない。)

ちなみに、下記にブラウザでアクセスすると、テキストとか画像がそのまま表示された。レスポンスヘッダの content-type は、画像だったら、image/webpとかにちゃんとなってた。

#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
    let path = Path::new("static/").join(file);
    NamedFile::open(path).await.ok()
}

リクエストガード (Request Guards)

Guard は、FromRequestトレイトを実装したやつをハンドラの引数に置くと、自動的にガードで設定されている条件をチェックしてくれる。引数の左から順にチェックする。複数のリクエストガードを引数に設定できて、全てのガードのチェックが通らないとアクセスできない。アクセスできるルートがないと 404 エラーが出る。

下記は「ヘッダにhogeがあって値が100」の場合のみ OK を出すガードです。

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

pub struct GuardA;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for GuardA {
    type Error = String;
    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        match req.headers().get_one("hoge") {
            Some(val) if val == "100" => Outcome::Success(GuardA),
            _ => Outcome::Forward(()),
        }
    }
}

Outcomeには、SuccessForward以外には、下記のようにFailureもある。Failureだと即座にエラー出して終わるけど、Forwardの場合は、他にマッチするルートがないかを確認視てくれるっぽい。1 つのルートに複数のリクエストガードを設定できるけど、複数設定した場合は、全部のガードが OK にならないとアクセスできない。左から順にチェックしていくから、OK だったら次のガードを確認していき、OK じゃなかった(マッチしなかった)場合、Failureならその場でエラーが出て終了するし、Forwardであれば、次のルートをチェックしにいく。

#[rocket::async_trait]
impl<'r> FromRequest<'r> for GuardA {
    type Error = String;
    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        match req.headers().get_one("hoge") {
            Some(val) if val == "100" => Outcome::Success(GuardA),
            _ => Outcome::Failure((Status::BadRequest, "Bad Request".to_string())),
        }
    }
}

次のルートのチェックというのは、下記のように rank を使うことで、同じメソッド・パスのものを複数作ることで実現できる。

#[get("/<hoge>")]
fn hoge(hoge: u8, _a: GuardA, _b: GuardB, _c:GuardC) -> String {
    format!("Hoge! {}", hoge)
}

#[get("/<hoge>", rank = 2)]
fn hoge2(hoge: u8, _a: GuardA) -> String {
    format!("Hoge 2! {}", hoge)
}

#[get("/<hoge>", rank = 3)]
fn hoge3(hoge: u8, _b: GuardB) -> String {
    format!("Hoge 3! {}", hoge)
}

CookieJarを使うと Cookie が取得できます。get()は、Option<&Cookie>を返します。

use rocket::http::CookieJar;

#[get("/")]
fn index(cookies: &CookieJar<'_>) -> Option<String> {
    cookies.get("hoge").map(|hoge| format!("HOGE: {}", hoge.value()))
}

尚、ハンドラが下記のように None を返すと 404 エラーになりました。

#[get("/none")]
fn none() -> Option<String> {
    None
}

リクエストデータ

formatで、リクエストヘッダのcontent-typeをチェックできる。dataは Body のデータの変数名を設定できる。ハンドラの引数でその変数名の型を設定したら、型に合わない場合はエラーが出る。ちなみに下記の場合、{hoge:10}のようにnumが含まれないデータを送ったら、「422 Unprocessable Entity」というエラーが出た。

use rocket::serde::json::Json;
use serde::Deserialize;

#[derive(Deserialize)]
struct Hoge {
    num: u8,
}

#[post("/", format = "json", data = "<hoge>")]
fn index(hoge: Json<Hoge>) -> String {
    format!("hoge {}", hoge.num)
}

エラー

デフォルトだとエラーは HTML で返されます。ただ、リクエストヘッダにAccept: application/jsonが設定されている場合は、json 形式で返してくれます。
また、エラー内容をカスタマイズするには下記のcatchを使います。

use rocket::http::Status;
use rocket::Request;

#[catch(default)]
fn default_catcher(status: Status, _request: &Request) -> String {
    let msg = match status.code {
        404 => "Not Found",
        500 => "Internal Server Error",
        _ => "Error",
    };
    format!("{} {}", status.code, msg)
}

#[launch]
fn rocket() -> _ {
    rocket::build().register("/", catchers![default_catcher])
}

下記のcatch(404)のようにステータスコード毎に設定することもできます。

#[catch(404)]
fn not_found() -> &'static str {
    "404 Not Found"
}

Discussion