Rustのhttp/httpsクライアントcurl

## 概要

Rustのhttp(s)クライアント、[curl](https://docs.rs/curl/latest/curl/index.html)を試したメモ。

同じ機能としては以下記事

https://qiita.com/ishishow/items/abacc371e62d2d2368b2

にあるように、hyperやreqwestなどが有名のようだが、両方とも「非同期処理(async)」での実装が前提のようで、お手軽には試しにくい印象。

一方[curl](https://docs.rs/curl/latest/curl/index.html)だと、asyncを使わずに手軽に行けそうだったので（とは言ってもRustだからやや面倒？）試してみた内容をご紹介する。

Rustのcurlのドキュメントによると、[libcurl](https://curl.se/libcurl/)をベースに作っているとのこと。
libcurlに関しては、以下の記事などでわかりやすく書かれていた。

https://qiita.com/suneo3476/items/a00113de2d005fa50440

C/C++で使うライブラリのようだが、行き詰まった場合はlibcurl関連のドキュメントも併せて見ると助けになりそう。

## 動作確認したバージョン情報

* rust : 1.51.0
* cargo : 1.51.0

```Cargo.toml
(中略)

[dependencies]
curl = "0.4.43"
```

## (1) 最も単純な実装

リクエストを投げて、レスポンスを標準出力するだけであれば、[curlのドキュメント](https://docs.rs/curl/latest/curl/index.html#the-easy-api)に書かれているように実装すればOK。全く同じ内容だが、以下にコピペ。
```rust
use std::io::{stdout, Write};
use curl::easy::Easy;

// Write the contents of rust-lang.org to stdout
let mut easy = Easy::new();
easy.url("https://www.rust-lang.org/").unwrap();
easy.write_function(|data| {
    stdout().write_all(data).unwrap();
    Ok(data.len())
}).unwrap();
easy.perform().unwrap();
```
ソース内で使っているメソッドの説明
* [write_function](https://docs.rs/curl/latest/curl/easy/struct.Easy.html#method.write_function)
    * 引数として、「レスポンスをどう処理するか」を定義したコールバック関数を取る（上記ソースはクロージャーで書かれている）。この関数の引数（上記ソースでいう``data``）はバイト型のスライス(``&[u8]``型)で、レスポンスデータが格納される模様。
    * コールバック関数の戻り値は必ず、レスポンスデータのバイト数を``Result<usize, WriteError>``型で返す必要あり。エラーを返すコードも自分で書く必要がありそう。
* [perform](https://docs.rs/curl/latest/curl/easy/struct.Easy2.html#method.perform)
    *  実際にリクエストを投げる関数。どのようなリクエストを投げるかは、この前に設定しておく必要あり。

## (2) レスポンスデータをソース内で扱う

(1)の例だとレスポンスをwrite_functionのコールバック関数内でしか扱えない。コールバック関数の外でもレスポンスデータを取得加工したい場合は、[こちら](https://docs.rs/curl/latest/curl/easy/struct.Easy.html#examples)の２番目の例にあるように、``transfer()``関数で型を``Easy``から``Transfer``に変換する。以下は同様のソース。
```rust
use curl::easy::Easy;

let mut data = Vec::new();
let mut handle = Easy::new();
handle.url("https://www.rust-lang.org/").unwrap();
{
    let mut transfer = handle.transfer();
    transfer.write_function(|new_data| {
        data.extend_from_slice(new_data);
        Ok(new_data.len())
    }).unwrap();
    transfer.perform().unwrap();
}
println!("{:?}", data);
```
ここで試しに``handle.transfer()``で型変換せずに同じことをやろうとすると、ビルドで以下のエラーがでる。
```
error[E0373]: closure may outlive the current function, but it borrows `data`, which is owned by the current function
  --> src/main.rs:35:29
   |
35 |       handle.write_function(|new_data| {
   |                             ^^^^^^^^^^ may outlive borrowed value `data`
36 |           data.extend_from_slice(new_data);
   |           ---- `data` is borrowed here
```
「``Easy``型だと、``data``が``new_data``よりも長く生き延びるのがNGで、``Transfer``だとそれを許してくてる」と筆者は理解した。


## (3) POSTでBODYをつけてリクエスト

以下のように、[post_fields_copy](https://docs.rs/curl/latest/curl/easy/struct.Easy2.html#method.post_fields_copy)メソッドにbodyを設定する。
```rust
use std::io::{stdout, Write};
use curl::easy::Easy;

let mut handle = Easy::new();
handle.url("https://reqres.in/api/users").unwrap();
handle.post(true).unwrap();

// リクエストのbodyを設定
let post_field_bytes = "{ \"name\": \"morpheus\", \"job\": \"leader\"}".as_bytes();
handle.post_fields_copy(post_field_bytes).unwrap();

// レスポンスを表示
handle.write_function(|data| {
  stdout().write_all(data).unwrap();
  Ok(data.len())
}).unwrap();

// リクエストを実行
handle.perform().unwrap();
```

やっていることは、curlの以下コマンドと同じ。
```
$ curl -X POST \
-d "{"name": "morpheus", "job": "leader"}" \
https://reqres.in/api/users
(以下は出力結果)
{"{name: morpheus, job: leader}":"","id":"230","createdAt":"2022-05-21T14:37:37.463Z"}
```

## (4) ヘッダーの設定

ヘッダーは[http_headers](https://docs.rs/curl/latest/curl/easy/struct.Easy2.html#method.http_headers)メソッドで指定。

```rust
use curl::easy::{Easy, List};

// 以下はcurlで「-H 'Foo: bar' -H 'Bar: baz'」のオプションを指定するのと同じ
let mut list = List::new();
list.append("Foo: bar").unwrap();
list.append("Bar: baz").unwrap();

let mut handle = Easy::new();
handle.url("https://www.rust-lang.org/").unwrap();
handle.http_headers(list).unwrap();
handle.perform().unwrap();
```

User-Agentの指定は別途、[useragent](https://docs.rs/curl/latest/curl/easy/struct.Easy2.html#method.useragent)メソッドを使う必要があるので注意。

```rust
use curl::easy::Easy;

let mut handle = Easy::new();
handle.url("https://www.rust-lang.org/").unwrap();

// curlの「-A」オプションを指定するのと同じ
let user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3864.0 Safari/537.36";
handle.useragent(user_agent).unwrap();
handle.perform().unwrap();
```

## (5) Cookieの設定

ここは結構ハマった。。。
デフォルトだとサーバから取得したCookieを保持しないみたい…。どうやら以下の関数でCookieを取得設定できる模様。
* [cookie_file](https://docs.rs/curl/latest/curl/easy/struct.Easy2.html#method.cookie_file)
    * 既に持っているCookieを読み込むファイルを指定。引数に""を設定すると、ファイル読みはせずにCookie取得の設定だけ有効化できる。
* [cookie_jar](https://docs.rs/curl/latest/curl/easy/struct.Easy2.html#method.cookie_jar)
    * 取得したCookieを書き込むファイルを指定。"-"を指定すると、ファイル書き込みはせずに標準出力になる。

以下、Cookie設定を有効にするソース例。``cookie.txt``にクッキーが保存される。

```rust
use curl::easy::Easy;

let mut handle = Easy::new();
handle.url("https://www.google.com").unwrap();

// Cookieの読み込み
handle.cookie_file("cookie.txt").unwrap();
// Cookieの書き込み
handle.cookie_jar("cookie.txt").unwrap();

handle.perform().unwrap();

// Cookieを出力して確認
println!("{:?}", handle.cookies());
```

## おわりに

何だかんだで高機能なのはたぶんhyperやreqwestなので、用途によってパッケージを使い分けることが肝要と思います。