🚛

Reqwestを用いたRustのhttp通信方法メモ

2024/04/16に公開


Rustでhttp通信をするためにreqwestを調べた結果の基本的機能の使用の流れとサンプルコードを備忘のため残しておきます。[reqwest 0.12.3]
cookie指定等のいくつかの個人的要点を抑えた簡潔なコードがあまり見当たらなかったので普通に使えるようになるまでなかなか苦労しました。

前提

  • reqwestを何も指定せずに使用すると非同期版となる
  • "blocking" featureを使用すると同期版を使用できる
    • しかし"blocking"のクライアントにはchunkメソッドが存在せず扱いが面倒
  • 非同期版を使用するデメリットは若干tokioの記述が増えるだけ
    • 非同期関数呼び出しにawaitを記述するだけであればほぼ同期版のように書ける
  • 以上より非同期版の使用を前提とする
Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["native-tls-alpn","cookies","gzip","brotli","deflate"]}
tokio = { version = "1", features = ["macros","rt"]}

基本的な使用の流れ

  1. ClientBuilderを使用してhttp通信を担当するClientを作成する
    • このタイミングでcookieとデフォルトヘッダーを指定する
  2. Clientを使用して1回の通信内容を作成するRequestBuilderを作成する
    • このタイミングで追加ヘッダーとボディ、クエリストリングを指定する
  3. RequestBuilderで通信を実行して応答となるResponseを得る
  4. Responseを取り扱う

サンプルコード

use reqwest::{Url, ClientBuilder, Client, RequestBuilder, Response, cookie::Jar, header::{self, HeaderMap, HeaderName, HeaderValue}};
use std::{collections::HashMap, sync::Arc, time::Duration};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let url = Url::parse("https://localhost/").unwrap();

    // create default headers
    let mut default_headers: HeaderMap = HeaderMap::new();
    default_headers.insert(header::ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate, br"));

    // create cookies
    let cookie_str = "cookie1=foo; cookie2=bar";
    let cookies = Arc::new(Jar::default());
    cookies.add_cookie_str(cookie_str, &url);

    // get client builder and gen client
    let client_builder: ClientBuilder = Client::builder(); // also can use ClientBuilder::new()
    let client: Client = client_builder
        .default_headers(default_headers)
        .cookie_provider(cookies)
        .timeout(Duration::from_secs(30))
        .build()
        .unwrap();

    // create onetime headers, and else
    let mut onetime_headers: HeaderMap = HeaderMap::new();
    onetime_headers.insert(HeaderName::try_from("Custom-Header").unwrap(), HeaderValue::from_static("header value"));
    let body = r#"{ "body_value": 1 }"#;
    let queries = HashMap::from([("q1".to_string(),"1".to_string()),("q2".to_string(),"2".to_string())]);

    // get request builder and send request
    let request_builder: RequestBuilder = client.post(url); // also can use get, or else
    let mut response: Response = request_builder
        .headers(onetime_headers)
        .body(body)
        .query(&queries)
        .send()
        .await
        .unwrap();

    // get response as string
    let res_str =  match response.headers().get(header::TRANSFER_ENCODING) {
        Some(v) if v == "chunked" => {
            let mut raw_res = Vec::new();
            while let Some(chunk) = response.chunk().await.unwrap() {
                chunk
                    .to_vec()
                    .into_iter()
                    .for_each(|x| raw_res.push(x));
            }
            String::from_utf8(raw_res).unwrap()
        },
        _ => response.text().await.unwrap()
    };

    println!("Response is : \n{}", res_str);
}

補足

  • "native-tls-alpn"のfeature指定で自動でhttp/2,http/1.1のどちらかで通信してくれるようになる
  • "gzip","brotli","deflate"のfeature指定で自動で各種形式でエンコード/デコードしてくれる
    • サンプルではACCEPT_ENCODINGを手動でヘッダーに追加しているが、自動でしてくれる
    • 不確かだがchunkedなdeflateエンコーディング応答はエラーとなるような挙動が確認された
  • ヘッダーを何も指定しないhttp/1.1のGETヘッダーは以下が設定される
    • Host
    • Content-Length: 0
    • Accept: */*

参考文献

Discussion