🐷

「actix-web」入門#3 リクエストボディ

2022/05/28に公開

前回の続き

前回「corsについて話します。(リクエストボディも含む)」と啖呵を切っていたのですが、リクエストボディが長くなりすぎたので次回にします。ごめんorz

今回はリクエストボディを試していきます。

webのリクエストボディ取得

詳しくリクエストボディの取得方法を確認してると以下みたく公式の記述がありました。
https://actix.rs/docs/request/

少し巻き戻って、TinyTemplateの例を確認します。

    HttpServer::new(|| {
        let mut tt = TinyTemplate::new();
        tt.add_template("index.html", INDEX).unwrap();
        tt.add_template("user.html", USER).unwrap();
        tt.add_template("error.html", ERROR).unwrap();

        App::new()
            .app_data(web::Data::new(tt))
            .wrap(middleware::Logger::default()) // enable logger
            .service(web::resource("/").route(web::get().to(index)))
            .service(web::scope("").wrap(error_handlers()))
    })

Appの関数の指定を細かく見ていくと、

関数「app_data」・・・configに「TinyTemplate」(SpringではThymereaf的な奴)を埋める。
 ※多分、Tomcatのコンテキストファイル的な奴です。
関数「wrap」・・・ログの設定してます。
関数「service」・・・各Webクエリパスにwebサービスを展開している

って感じです。

で、鋭い人はすでに気づいてると思いますが、
RequestBodyの値を「web.get()」ではGETリクエストを指定しています。
※前回の私は気づいてませんでした(*'▽')Σ\( ̄ー ̄;)

そこで思ったことはこうです。
…あれ?わざわざRequestParameterかます必要ないのでは!?

ってことで、RequestBodyから直接TinyTemplateでhtmlへ解釈して表示してみます。

関数「index」の下にポスト情報読取用Objectと関数「index2」を作成します。

main.rs
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Info {
    name: String,
}

/// extract `Info` using serde
pub async fn index2(
    tmpl: web::Data<TinyTemplate<'_>>,
    info: web::Form<Info>
) -> Result<HttpResponse, Error> {
    // submitted form
    let ctx = json!({
        "name" : info.name.to_owned(),
        "text" : "Welcome!".to_owned()
    });
    let s = {
        tmpl.render("user.html", &ctx)
            .map_err(|_| error::ErrorInternalServerError("Template error"))?
    };
    Ok(HttpResponse::Ok().content_type("text/html").body(s))
}

Appの定義は以下みたいにWebルートパス「"/"」に対してgetした場合とpostした場合の記述を施しておきます。
※関数「service」でなく関数「route」を直接使っていることに注意してください。

main.rs
        App::new()
            .app_data(web::Data::new(tt))
            .wrap(middleware::Logger::default()) // enable logger
            .route("/", web::get().to(index))
            .route("/", web::post().to(index2))
            .service(web::scope("").wrap(error_handlers()))

あと「index.html」はポスト処理のため「method="POST"」を追記しておきます。

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Actix Web</title>
</head>
<body>
  <h1>Welcome!</h1>
  <p>
    <h3>What is your name?</h3>
    <form method="POST">
      <input type="text" name="name" /><br/>
      <p><input type="submit"></p>
    </form>
  </p>
</body>
</html>

これで「cargo run」して、ブラウザから確認してみるとこんな感じです。

前回との違いはURLが変わってないところです。

上記の例だとGetでユーザ入力画面表示して、Postでログイン後の画面を表示するみたくできるので、棲み分けができていい感じです。
※もちろん、パラメータにページ番号を渡すみたいな考え方もあるので一概に上記のようにすることがすべて良いとは限りません。

GET、POSTの使い分けのまとめ

GET、POSTを使い分けるパターンは2つあります。

1つ目はアトリビュートでリクエストを指定するパターン

basics/main.rs
/// simple index handler
#[get("/welcome")]
async fn welcome(req: HttpRequest, session: Session) -> Result<HttpResponse> {
    ...
}
...
#[actix_web::main]
async fn main() -> io::Result<()> {
	...
        App::new()
            // register simple route, handle all methods
            .service(welcome)
            // default
            .default_service(web::to(default_handler))
	...
}

2つ目は関数「route」でリクエストを直接指定するパターン

main.rs
.service(web::resource("/").route(web::get().to(index)))

今回はクエリパス「localhost:8080/」に対して、get、postを使い分けする必要があったため、2つ目の中でも特殊なパターンを用いています。

特殊パターン

main.rs
.route("/", web::get().to(index))

上記の特殊パターンは以下のサイトを参考にしました。
https://auth0.com/blog/build-an-api-in-rust-with-jwt-authentication-using-actix-web/

1つ目のアトリビュートでリクエストを指定する方法でも実装できるのかもしれませんが、自分の力量では不能でした。
できる方法知ってる方いたらコメント欄で教えてください。

GET、POST用関数の引数のまとめ

GETとかPOSTとかの具体的な処理内容を定義する関数について、以下の順に指定するといいようです。
引数1:app_data「config情報」
引数2:URLのクエリパラメータ
引数3:リクエストボディの内容(submitされたFORM)

具体例)

pub async fn index(
    tmpl: web::Data<TinyTemplate<'_>>, // app_data「config情報」
    query: web::Query<HashMap<String, String>>, // URLのクエリパラメータ
    info: web::Form<Info> // リクエストボディの内容
) -> Result<HttpResponse, Error> {
    ...
}

余談

今回は以下の部分で結構沼にはまってしまいました。

main.rs
        App::new()
            .app_data(web::Data::new(tt))
            .wrap(middleware::Logger::default()) // enable logger
            .route("/", web::get().to(index))
            .route("/", web::post().to(index2))
            .service(web::scope("").wrap(error_handlers()))

最初、以下みたくしてソース編集してテストしてました。

main.rs
        App::new()
            .app_data(web::Data::new(tt))
            .wrap(middleware::Logger::default()) // enable logger
            .service(web::resource("/").route(web::get().to(index)))
            .service(web::resource("/").route(web::post().to(index2)))
            .service(web::scope("").wrap(error_handlers()))

四苦八苦して、curlしたり、jmeter使ったりしてデバッグしたのですが、serviceが重複するクエリパスに対して指定されている場合、ちゃんと起動できないみたいで「POST」の関数までたどり着きさえしませんでした。

特殊パターンのとこで紹介したサイトにはマジで助けられました。

次回

corsについて話します。(Jsonも含む)

次回以降は以下のいずれかをやっていく感じです。
・セッション周りとかの解説
・db周りとかの解説
・test周りとかの解説
・tls周りとかの解説

Discussion