Rust + Rocket0.5.0-rc でCORS対応

# はじめに

**(2021/06/18追記)初出のコードではpreflightリクエストに対して正しく動きませんでしたので少し修正しています。**


前回投稿した記事の続きです。


https://qiita.com/h008/items/71ae7509edf2aaaeeb3b

Rocketで作ったAPIサーバーをCORSに対応できるように修正します。
下記のURLを参考にしていますがRocketv0.5で若干仕様が変わっていますのでそのままではコンパイルできません。

https://stackoverflow.com/questions/62412361/how-to-set-up-cors-or-options-for-rocket-rs

Rocket v0.5.0-rc.1に対応させたコードを紹介します。

# サンプルコード

はじめに試したコード（失敗例です）

``` Rust:src/main.rs
#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

//models.rsとroutes.rsは前回の記事を参考にしてください。
mod models;
mod routes;

//一部追加しています
use rocket::{Build, Rocket};
use rocket::routes;
use rocket::fairing::{self, AdHoc,Fairing,Info,Kind};
use rocket::{Request,Response};
use rocket::http::Header;

use dotenv::dotenv;
use routes::*;

//ここを追加します
struct CORS;

#[rocket::async_trait]
impl Fairing for CORS {
    fn info(&self) -> Info {
        Info {
            name: "Add CORS headers to requests",
            kind: Kind::Response
        }
    }

//とりあえずサンプルのためAccess-Control-Allow-Origin:"*"としていますが、実運用ではちゃんと指定しましょう
    async fn on_response <'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>){
        response.set_header(Header::new("Access-Control-Allow-Origin","*"));
        response.set_header(Header::new("Access-Control-Allow-Methods","POST,GET,PATCH,OPTIONS"));
        response.set_header(Header::new("Access-Control-Allow-Headers","*"));
        response.set_header(Header::new("Access-Control-Allow-Credentioals","true"));        
    }
}


async fn init_db(rocket: Rocket<Build>) -> fairing::Result {
    let opt = sqlx::mysql::MySqlConnectOptions::new()
        .host(&std::env::var("MYSQL_HOST").unwrap())
        .ssl_mode(sqlx::mysql::MySqlSslMode::Disabled)
        .database(&std::env::var("MYSQL_DB").unwrap())
        .username(&std::env::var("MYSQL_USER").unwrap())
        .password(&std::env::var("MYSQL_PASSWORD").unwrap());

    let db = match sqlx::MySqlPool::connect_with(opt).await {
        Ok(pool) => pool,
        Err(e) => {
            error!("Failed to connenct to SQLx database: {}", e);
            return Err(rocket);
        }
    };
    Ok(rocket.manage(db))
}

#[launch]
fn rocket() -> _ {
    dotenv().ok();
    
//追加したコードをattachします。
    rocket::build()
        .attach(CORS)
        .attach(AdHoc::try_on_ignite("SQLx Database", init_db))
        .mount("/", routes![index,users])
}

``` 

当初、ここまでの状態で完成としていましたが、実環境でブラウザからfetchで試してみたところ、corsエラーでうまく動きませんでした。

# Preflite (OPTION)リクエストに対応する

コンソールを見てみると、CurlでGETリクエストを発行した場合は上のコードで準備したAccess-Control-Allow-Originヘッダが返ってくるのですが、ブラウザからのリクエストではこれらのヘッダが返ってきていないようでした。

ブラウザからのアクセスの場合はGETの前にPreflightリクエストというものが発行されます。
このリクエストに使われるOPTIONメソッドに対してヘッダを準備する必要がありそうです。

https://developer.mozilla.org/ja/docs/Glossary/Preflight_request

とりあえずroutes.rsファイルに追記します。

``` Rust:src/routes.rs
use crate::models::*;
use rocket::http::{Header, Status};
use rocket::request::{FromRequest, Outcome, Request};
use rocket::response;
use rocket::serde::json::Json;
use rocket::State;

pub struct ApiKey<'r>(&'r str);

#[derive(Debug)]
pub enum ApiKeyError {
    Missing,
    Invalid,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for ApiKey<'r> {
    type Error = ApiKeyError;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        warn!("{:?}", req.headers().get_one("x-api-key"));
        fn is_valid(key: &str) -> bool {
            key == std::env::var("API_KEY").unwrap()
        }

        match req.headers().get_one("x-api-key") {
            None => Outcome::Failure((Status::BadRequest, ApiKeyError::Missing)),
            Some(key) if is_valid(key) => Outcome::Success(ApiKey(key)),
            Some(_) => Outcome::Failure((Status::BadRequest, ApiKeyError::Invalid)),
        }
    }
}


//ここから追記
//Preflightリクエストに対するレスポンスの構造体を用意します
pub struct PfResponse;

// 必要なヘッダを含むレスポンスを返すResponderをインプリメントします
impl<'r> response::Responder<'r, 'static> for PfResponse {
    fn respond_to(self, _request: &'r Request<'_>) -> response::Result<'static> {
        response::Response::build()
            .header(Header::new(
                "Access-Control-Allow-Origin",
                "*",
            ))
            .header(Header::new(
                "Access-Control-Allow-Methods",
                "POST,GET,PATCH,OPTIONS,TRACE",
            ))
            .header(Header::new("Access-Control-Allow-Headers", "x-api-key"))
            .header(Header::new("Access-Control-Allow-Credentials", "true"))
            .ok()
    }
}


//OPTIONメソッドでのリクエストをキャッチします。
#[options("/<_p..>")]
pub fn preflight( _p: std::path::PathBuf) -> PfResponse {
    PfResponse
}

#[get("/users")]
pub async fn users(
    _key: ApiKey<'_>,
    db: &State<sqlx::MySqlPool>,
) -> Json<ApiResponse<Vec<Users>>> {
    let r = fetch_all_users(&db).await;

    match r {
        Ok(val) => Json(ApiResponse {
            success: true,
            data: ResBody::OK(val),
        }),
        Err(e) => Json(ApiResponse {
            success: false,
            data: ResBody::NG(e.to_string()),
        }),
    }
}

```

main.rsから不要なコードを削除して、ルートを追加します。

``` Rust:src/main.rs
#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

use rocket::{Build, Rocket};
use rocket::routes;
use rocket::fairing::{self, AdHoc};

use dotenv::dotenv;

//models.rsとroutes.rsは前回の記事を参考にしてください。
mod models;
mod routes;
use routes::*;

async fn init_db(rocket: Rocket<Build>) -> fairing::Result {
    let opt = sqlx::mysql::MySqlConnectOptions::new()
        .host(&std::env::var("MYSQL_HOST").unwrap())
        .ssl_mode(sqlx::mysql::MySqlSslMode::Disabled)
        .database(&std::env::var("MYSQL_DB").unwrap())
        .username(&std::env::var("MYSQL_USER").unwrap())
        .password(&std::env::var("MYSQL_PASSWORD").unwrap());

    let db = match sqlx::MySqlPool::connect_with(opt).await {
        Ok(pool) => pool,
        Err(e) => {
            error!("Failed to connenct to SQLx database: {}", e);
            return Err(rocket);
        }
    };
    Ok(rocket.manage(db))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(AdHoc::try_on_ignite("SQLx Database", init_db))
        .mount("/", routes![index,users,preflight])
}
``` 

# まとめ

少し冗長な気もしますが、とりあえずこれで動きました。

まだRustもCORSも理解が浅いですが、情報を漁りながら手探りでコードを書いていると何かと勉強になります。