reqwest
Rust向けの使いやすく強力なHTTPクライアント。async/await完全サポート、Hyper基盤の高性能、豊富な機能セット。自動JSON処理、認証、プロキシ、リダイレクト、Cookie管理、ファイルアップロード機能を提供。Rustエコシステムで最も人気のHTTPクライアント。
GitHub概要
seanmonstar/reqwest
An easy and powerful Rust HTTP Client
トピックス
スター履歴
ライブラリ
Reqwest
概要
Reqwestは「人間工学に基づいたRust向けHTTPクライアント」として開発された、RustエコシステムでdeFactoの地位を確立している高レベルHTTPクライアントライブラリです。「使いやすさ」を重視して設計され、非同期(async)とブロッキング(blocking)の両方のAPIを提供。接続プーリング、TLS、リダイレクト、クッキー、認証システム、JSON/フォーム処理など、企業レベルのHTTP通信要件を満たす包括的な機能を提供し、RustのWebアプリケーション開発において必須のライブラリとして広く採用されています。
詳細
Reqwest 2025年版は、Rust HTTPクライアントの決定版として確固たる地位を維持し続けています。Tokioランタイム上に構築されたノンブロッキングI/O(async API)と、従来のブロッキングI/O(blocking API)の両方を提供することで、多様なプログラミングモデルに対応。hyperなどの低レベルライブラリの上に構築された高レベルAPIにより、複雑なHTTP通信をシンプルかつ直感的に実装可能。接続プーリング、TLS/SSL設定、プロキシサポート、リダイレクト処理、セッション管理、WASM対応など、現代的なWebアプリケーション開発に必要な全機能を統合提供します。
主な特徴
- 双方向API設計: async/awaitとブロッキングの両対応による柔軟な利用
- 包括的認証サポート: Basic、Bearer、プロキシ認証の完全対応
- マルチフォーマット対応: JSON、URLエンコード、マルチパートフォーム処理
- 高度なTLS制御: 複数TLSバックエンド選択と詳細SSL設定
- 効率的な接続管理: 自動接続プーリングとセッション最適化
- WASM完全対応: ブラウザ環境でのFetch API統合
メリット・デメリット
メリット
- Rustエコシステムでの圧倒的な普及率と豊富な学習リソース
- 人間工学に基づいた高レベルAPIによる高い開発効率
- async/awaitとブロッキングの両対応による柔軟なプログラミングモデル
- 包括的なHTTP機能による企業レベル対応(TLS、認証、プロキシ等)
- 優れたパフォーマンスと効率的な接続プーリング
- WASM対応によるフロントエンド開発との統合性
デメリット
- 多機能なためデフォルト依存関係が多く、バイナリサイズが増大
- WASM環境では一部機能(TLS、クッキー、blocking API)が制限
- 非同期ランタイム内でblocking APIを使用すると潜在的なパニック
- 低レベル制御が必要な場合はhyper等の直接利用が必要
- フィーチャフラグの設定を間違えると機能不全になる可能性
- 初期学習時にasync/awaitの理解が必要
参考ページ
書き方の例
インストールと基本セットアップ
# Cargo.toml - 基本設定
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
# フル機能有効化版
[dependencies]
reqwest = { version = "0.12", features = ["json", "multipart", "stream", "gzip", "brotli"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
# ブロッキングAPIのみ使用
[dependencies]
reqwest = { version = "0.12", features = ["blocking", "json"] }
# WASM対応版
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
基本的なリクエスト(GET/POST/PUT/DELETE)
use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: Option<u32>,
name: String,
email: String,
age: u32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 基本的なGETリクエスト(ショートカット)
let response = reqwest::get("https://api.example.com/users")
.await?;
println!("Status: {}", response.status());
println!("Headers: {:#?}", response.headers());
// JSONレスポンスの処理
let body = response.text().await?;
println!("Body: {}", body);
// 構造化データとしてのGETリクエスト
let users: Vec<User> = reqwest::get("https://api.example.com/users")
.await?
.json()
.await?;
println!("Users: {:#?}", users);
// Clientインスタンスの作成(複数リクエスト推奨)
let client = reqwest::Client::new();
// クエリパラメータ付きGETリクエスト
let users = client
.get("https://api.example.com/users")
.query(&[("page", "1"), ("limit", "10"), ("sort", "created_at")])
.send()
.await?
.json::<Vec<User>>()
.await?;
// POSTリクエスト(JSON送信)
let new_user = User {
id: None,
name: "田中太郎".to_string(),
email: "[email protected]".to_string(),
age: 30,
};
let created_user = client
.post("https://api.example.com/users")
.header("Authorization", "Bearer your-token")
.json(&new_user)
.send()
.await?
.json::<User>()
.await?;
println!("Created user: {:#?}", created_user);
// POSTリクエスト(フォームデータ送信)
let mut form_data = HashMap::new();
form_data.insert("username", "testuser");
form_data.insert("password", "secret123");
let login_response = client
.post("https://api.example.com/login")
.form(&form_data)
.send()
.await?;
// PUTリクエスト(データ更新)
let user_id = created_user.id.unwrap();
let updated_data = User {
id: Some(user_id),
name: "田中次郎".to_string(),
email: "[email protected]".to_string(),
age: 31,
};
let updated_user = client
.put(&format!("https://api.example.com/users/{}", user_id))
.header("Authorization", "Bearer your-token")
.json(&updated_data)
.send()
.await?
.json::<User>()
.await?;
// DELETEリクエスト
let delete_response = client
.delete(&format!("https://api.example.com/users/{}", user_id))
.header("Authorization", "Bearer your-token")
.send()
.await?;
if delete_response.status().is_success() {
println!("User deleted successfully");
}
Ok(())
}
// ブロッキングAPIの使用例
fn blocking_example() -> Result<(), Box<dyn std::error::Error>> {
// ブロッキングクライアント(非async環境)
let client = reqwest::blocking::Client::new();
let response = client
.get("https://api.example.com/users")
.header("User-Agent", "MyRustApp/1.0")
.send()?;
let users: Vec<User> = response.json()?;
println!("Users: {:#?}", users);
Ok(())
}
高度な設定とカスタマイズ(認証、タイムアウト、TLS等)
use reqwest::{Client, ClientBuilder, header, Certificate};
use std::time::Duration;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// カスタムクライアントビルダー設定
let client = ClientBuilder::new()
.timeout(Duration::from_secs(30)) // 全体タイムアウト
.connect_timeout(Duration::from_secs(10)) // 接続タイムアウト
.tcp_keepalive(Duration::from_secs(60)) // TCP KeepAlive
.pool_idle_timeout(Duration::from_secs(90)) // 接続プール空き時間
.pool_max_idle_per_host(10) // ホスト毎最大空き接続数
.user_agent("MyRustApp/1.0 (Reqwest)") // User-Agentヘッダー
.gzip(true) // gzip圧縮有効化
.brotli(true) // Brotli圧縮有効化
.deflate(true) // Deflate圧縮有効化
.cookie_store(true) // クッキーストア有効化
.redirect(reqwest::redirect::Policy::limited(10)) // リダイレクト制限
.build()?;
// カスタムヘッダー設定
let mut headers = header::HeaderMap::new();
headers.insert("X-API-Version", "v2".parse()?);
headers.insert("Accept", "application/json".parse()?);
headers.insert("Accept-Language", "ja-JP,en-US".parse()?);
headers.insert("X-Request-ID", "req-12345".parse()?);
let response = client
.get("https://api.example.com/data")
.headers(headers)
.send()
.await?;
// Basic認証
let response = client
.get("https://api.example.com/private")
.basic_auth("username", Some("password"))
.send()
.await?;
// Bearer Token認証
let response = client
.get("https://api.example.com/protected")
.bearer_auth("your-jwt-token")
.send()
.await?;
// プロキシ設定
let proxy = reqwest::Proxy::http("http://proxy.example.com:8080")?
.basic_auth("proxy_user", "proxy_pass");
let client_with_proxy = ClientBuilder::new()
.proxy(proxy)
.build()?;
// TLS/SSL詳細設定
let cert = fs::read("client.crt")?;
let cert = Certificate::from_pem(&cert)?;
let tls_client = ClientBuilder::new()
.add_root_certificate(cert) // カスタムCA証明書
.danger_accept_invalid_certs(false) // 証明書検証有効
.danger_accept_invalid_hostnames(false) // ホスト名検証有効
.min_tls_version(reqwest::tls::Version::TLS_1_2) // 最小TLSバージョン
.use_rustls_tls() // RustlsのTLS使用
.build()?;
// タイムアウト詳細制御
let response = client
.get("https://api.example.com/slow-endpoint")
.timeout(Duration::from_secs(60)) // このリクエストのみのタイムアウト
.send()
.await?;
Ok(())
}
// マルチパートフォームのファイルアップロード
async fn file_upload_example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// マルチパートフォーム作成
let form = reqwest::multipart::Form::new()
.text("description", "アップロードファイル")
.text("category", "documents")
.file("file", "/path/to/document.pdf")
.await?;
let response = client
.post("https://api.example.com/upload")
.bearer_auth("your-token")
.multipart(form)
.send()
.await?;
let upload_result: serde_json::Value = response.json().await?;
println!("Upload result: {:#?}", upload_result);
Ok(())
}
// ストリーミング処理
async fn streaming_example() -> Result<(), Box<dyn std::error::Error>> {
use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
let client = Client::new();
let response = client
.get("https://api.example.com/large-file.zip")
.send()
.await?;
// ストリーミングダウンロード
let mut file = tokio::fs::File::create("downloaded_file.zip").await?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
}
println!("File downloaded successfully");
Ok(())
}
エラーハンドリングとリトライ機能
use reqwest::{Client, Error, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug)]
enum APIError {
NetworkError(Error),
HTTPError(StatusCode),
TimeoutError,
UnauthorizedError,
NotFoundError,
ServerError,
}
impl From<Error> for APIError {
fn from(err: Error) -> Self {
if err.is_timeout() {
APIError::TimeoutError
} else {
APIError::NetworkError(err)
}
}
}
// 包括的なエラーハンドリング
async fn safe_request(
client: &Client,
url: &str,
) -> Result<reqwest::Response, APIError> {
let response = client.get(url).send().await?;
match response.status() {
StatusCode::OK => Ok(response),
StatusCode::UNAUTHORIZED => Err(APIError::UnauthorizedError),
StatusCode::NOT_FOUND => Err(APIError::NotFoundError),
status if status.is_server_error() => Err(APIError::ServerError),
status => Err(APIError::HTTPError(status)),
}
}
// リトライ機能付きリクエスト
async fn request_with_retry<T>(
client: &Client,
url: &str,
max_retries: u32,
backoff_factor: u64,
) -> Result<T, APIError>
where
T: serde::de::DeserializeOwned,
{
for attempt in 0..=max_retries {
match safe_request(client, url).await {
Ok(response) => {
return response
.json::<T>()
.await
.map_err(|e| APIError::NetworkError(e));
}
Err(APIError::ServerError) | Err(APIError::TimeoutError) => {
if attempt == max_retries {
return Err(APIError::ServerError);
}
let wait_time = backoff_factor * 2_u64.pow(attempt);
println!("リトライ {} / {} - {}秒後に再試行", attempt + 1, max_retries, wait_time);
sleep(Duration::from_secs(wait_time)).await;
}
Err(e) => return Err(e), // 永続的なエラー
}
}
Err(APIError::ServerError)
}
// 詳細なエラー処理例
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// error_for_statusメソッドを使った簡潔なエラーハンドリング
let response = client
.get("https://api.example.com/data")
.send()
.await?
.error_for_status()?; // 4xx/5xxでエラーに変換
let data: serde_json::Value = response.json().await?;
// カスタムエラー処理
match request_with_retry::<Vec<serde_json::Value>>(&client, "https://api.example.com/users", 3, 1).await {
Ok(users) => println!("ユーザー取得成功: {} 件", users.len()),
Err(APIError::UnauthorizedError) => {
println!("認証エラー: トークンを確認してください");
}
Err(APIError::NotFoundError) => {
println!("リソースが見つかりません");
}
Err(APIError::TimeoutError) => {
println!("リクエストタイムアウト: ネットワーク状況を確認してください");
}
Err(APIError::ServerError) => {
println!("サーバーエラー: しばらく時間をおいて再度お試しください");
}
Err(APIError::HTTPError(status)) => {
println!("予期しないHTTPエラー: {}", status);
}
Err(APIError::NetworkError(err)) => {
println!("ネットワークエラー: {}", err);
}
}
Ok(())
}
// 回路ブレーカー実装例
use std::sync::{Arc, atomic::{AtomicU32, Ordering}};
struct CircuitBreaker {
failure_count: Arc<AtomicU32>,
failure_threshold: u32,
is_open: Arc<AtomicU32>,
}
impl CircuitBreaker {
fn new(failure_threshold: u32) -> Self {
Self {
failure_count: Arc::new(AtomicU32::new(0)),
failure_threshold,
is_open: Arc::new(AtomicU32::new(0)),
}
}
async fn call<F, Fut, T>(&self, f: F) -> Result<T, APIError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T, APIError>>,
{
if self.is_open.load(Ordering::Relaxed) == 1 {
return Err(APIError::ServerError);
}
match f().await {
Ok(result) => {
self.failure_count.store(0, Ordering::Relaxed);
self.is_open.store(0, Ordering::Relaxed);
Ok(result)
}
Err(e) => {
let failures = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
if failures >= self.failure_threshold {
self.is_open.store(1, Ordering::Relaxed);
println!("回路ブレーカーが作動しました");
}
Err(e)
}
}
}
}
並行処理とパフォーマンス最適化
use reqwest::Client;
use tokio::task::JoinSet;
use futures_util::{future::join_all, stream::{self, StreamExt}};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Arc::new(Client::new());
// 複数URLの並行取得(join_all使用)
let urls = vec![
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments",
"https://api.example.com/categories",
];
let requests = urls.iter().map(|url| {
let client = client.clone();
let url = url.to_string();
async move {
client.get(&url).send().await?.text().await
}
});
let results = join_all(requests).await;
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(body) => println!("URL {} 成功: {} 文字", i, body.len()),
Err(e) => println!("URL {} エラー: {}", i, e),
}
}
// JoinSetを使用した並行処理
let mut join_set = JoinSet::new();
for (i, url) in urls.iter().enumerate() {
let client = client.clone();
let url = url.to_string();
join_set.spawn(async move {
let result = client.get(&url).send().await?.json::<serde_json::Value>().await?;
Ok::<(usize, serde_json::Value), reqwest::Error>((i, result))
});
}
while let Some(result) = join_set.join_next().await {
match result {
Ok(Ok((index, data))) => println!("タスク {} 完了: {} keys", index, data.as_object().map_or(0, |o| o.len())),
Ok(Err(e)) => println!("リクエストエラー: {}", e),
Err(e) => println!("タスクエラー: {}", e),
}
}
// ストリーム処理による制御された並行実行
let concurrent_limit = 5;
let results: Vec<_> = stream::iter(urls)
.map(|url| {
let client = client.clone();
async move {
client.get(url).send().await?.text().await
}
})
.buffer_unordered(concurrent_limit) // 並行実行数を制限
.collect()
.await;
let successful_count = results.iter().filter(|r| r.is_ok()).count();
println!("成功: {} / {} リクエスト", successful_count, urls.len());
Ok(())
}
// ページネーション対応の全データ取得
async fn fetch_all_pages<T>(
client: &Client,
base_url: &str,
auth_token: &str,
) -> Result<Vec<T>, Box<dyn std::error::Error>>
where
T: serde::de::DeserializeOwned,
{
let mut all_data = Vec::new();
let mut page = 1;
let per_page = 100;
loop {
let response = client
.get(base_url)
.bearer_auth(auth_token)
.query(&[("page", page), ("per_page", per_page)])
.send()
.await?
.json::<serde_json::Value>()
.await?;
let items = response["items"].as_array()
.ok_or("Invalid response format")?;
if items.is_empty() {
break;
}
for item in items {
let typed_item: T = serde_json::from_value(item.clone())?;
all_data.push(typed_item);
}
println!("ページ {} 取得完了: {} 件", page, items.len());
// 次のページがあるかチェック
if response["has_more"].as_bool().unwrap_or(false) == false {
break;
}
page += 1;
// API負荷軽減のための待機
tokio::time::sleep(Duration::from_millis(100)).await;
}
println!("総取得件数: {} 件", all_data.len());
Ok(all_data)
}
// 接続プールのカスタマイズ
fn create_optimized_client() -> Result<Client, reqwest::Error> {
ClientBuilder::new()
.pool_max_idle_per_host(20) // ホスト当たり最大アイドル接続数
.pool_idle_timeout(Duration::from_secs(120)) // アイドルタイムアウト
.tcp_keepalive(Duration::from_secs(60)) // TCP KeepAlive
.tcp_nodelay(true) // Nagleアルゴリズム無効化
.http2_prior_knowledge() // HTTP/2を優先使用
.http2_keep_alive_interval(Duration::from_secs(30))
.http2_keep_alive_timeout(Duration::from_secs(10))
.build()
}
WASM統合と実用例
// WASM環境での使用例
#[cfg(target_arch = "wasm32")]
mod wasm_example {
use reqwest::Client;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::console;
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
spawn_local(async {
if let Err(e) = fetch_data().await {
console::log_1(&format!("Error: {:?}", e).into());
}
});
}
async fn fetch_data() -> Result<(), reqwest::Error> {
let client = Client::new();
// WASM環境でのGETリクエスト(Fetch API使用)
let response = client
.get("https://api.example.com/data")
.header("Accept", "application/json")
.send()
.await?;
let data: serde_json::Value = response.json().await?;
console::log_1(&format!("Data: {:?}", data).into());
Ok(())
}
// ブラウザのCORS対応
async fn cors_request() -> Result<(), reqwest::Error> {
let client = Client::new();
let response = client
.post("https://api.example.com/data")
.header("Content-Type", "application/json")
.header("Access-Control-Allow-Origin", "*")
.json(&serde_json::json!({"message": "Hello from WASM"}))
.send()
.await?;
console::log_1(&format!("Status: {}", response.status()).into());
Ok(())
}
}
// 統合テスト用のモック化例
#[cfg(test)]
mod tests {
use super::*;
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};
#[tokio::test]
async fn test_api_integration() {
// モックサーバー起動
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!([
{"id": 1, "name": "Alice", "email": "[email protected]", "age": 30},
{"id": 2, "name": "Bob", "email": "[email protected]", "age": 25}
])))
.mount(&mock_server)
.await;
let client = Client::new();
let url = format!("{}/users", mock_server.uri());
let users: Vec<serde_json::Value> = client
.get(&url)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(users.len(), 2);
assert_eq!(users[0]["name"], "Alice");
}
}