Surf
Rustの非同期HTTP・WebSocketクライアント。モジュラー設計と強力なミドルウェアシステムが特徴。async-std生態系に最適化され、拡張可能な設計により柔軟なHTTP通信を実現。curl、hyper等複数のバックエンドに対応。
概要
Surfは、使いやすさと柔軟性を重視して設計されたRustの非同期HTTPクライアントライブラリです。複数のHTTPバックエンドをサポートし、強力なミドルウェアシステムを備えています。async-stdベースで構築され、完全にストリーミング対応のリクエストとレスポンスを提供します。
主な特徴
- マルチバックエンドサポート: curl、hyper、async-h1、WASMなど複数のバックエンドに対応
- ミドルウェアシステム: 拡張可能なリクエスト/レスポンス処理パイプライン
- 完全な非同期サポート: async/awaitを使用したモダンなRust非同期プログラミング
- ストリーミング対応: 大きなファイルの効率的な処理
- TLSサポート: native-tlsとrustlsの両方に対応
- 接続プーリング: 効率的な接続の再利用
インストール
Cargo.toml
[dependencies]
# デフォルト(curl-client、middleware-logger、encoding)
surf = "2.3"
# 特定のバックエンドを使用する場合
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
# Tokioランタイムを使用する場合
surf = { version = "2.3", features = ["tokio"] }
利用可能な機能フラグ
curl-client: curlをバックエンドとして使用(デフォルト)h1-client: async-h1をバックエンドとして使用hyper-client: hyperをバックエンドとして使用wasm-client: WebAssembly環境でwindow.fetchを使用middleware-logger: ロギングミドルウェア(デフォルト)encoding: エンコーディングサポート(デフォルト)
基本的な使い方
シンプルなGETリクエスト
use surf;
#[async_std::main]
async fn main() -> surf::Result<()> {
let mut res = surf::get("https://httpbin.org/get").await?;
println!("{}", res.body_string().await?);
Ok(())
}
Clientを使用した永続的な接続
use std::convert::TryInto;
use std::time::Duration;
use surf::{Client, Config};
use surf::Url;
#[async_std::main]
async fn main() -> surf::Result<()> {
let client: Client = Config::new()
.set_base_url(Url::parse("https://api.example.com")?)
.set_timeout(Some(Duration::from_secs(5)))
.try_into()?;
let mut res = client.get("/users").await?;
println!("{}", res.body_string().await?);
Ok(())
}
実装例
1. 各種HTTPメソッドの実装
use surf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[async_std::main]
async fn main() -> surf::Result<()> {
// GETリクエスト
let mut get_res = surf::get("https://api.example.com/users/1").await?;
let user: User = get_res.body_json().await?;
println!("GET: {:?}", user);
// POSTリクエスト(JSON)
let new_user = User {
id: 0,
name: "新しいユーザー".to_string(),
email: "[email protected]".to_string(),
};
let mut post_res = surf::post("https://api.example.com/users")
.body_json(&new_user)?
.await?;
println!("POST Status: {}", post_res.status());
// PUTリクエスト
let update_user = User {
id: 1,
name: "更新されたユーザー".to_string(),
email: "[email protected]".to_string(),
};
let mut put_res = surf::put("https://api.example.com/users/1")
.body_json(&update_user)?
.await?;
println!("PUT Status: {}", put_res.status());
// DELETEリクエスト
let mut delete_res = surf::delete("https://api.example.com/users/1").await?;
println!("DELETE Status: {}", delete_res.status());
Ok(())
}
2. エラーハンドリング
use surf;
use surf::StatusCode;
#[derive(Debug)]
enum ApiError {
NetworkError(surf::Error),
NotFound,
Unauthorized,
ServerError(String),
}
impl From<surf::Error> for ApiError {
fn from(err: surf::Error) -> Self {
ApiError::NetworkError(err)
}
}
async fn fetch_user(id: u32) -> Result<String, ApiError> {
let url = format!("https://api.example.com/users/{}", id);
let mut res = surf::get(&url).await?;
match res.status() {
StatusCode::Ok => Ok(res.body_string().await?),
StatusCode::NotFound => Err(ApiError::NotFound),
StatusCode::Unauthorized => Err(ApiError::Unauthorized),
status if status.is_server_error() => {
let error_msg = res.body_string().await.unwrap_or_default();
Err(ApiError::ServerError(error_msg))
}
_ => {
let err = surf::Error::from_str(
StatusCode::InternalServerError,
format!("Unexpected status: {}", res.status())
);
Err(ApiError::NetworkError(err))
}
}
}
#[async_std::main]
async fn main() {
match fetch_user(1).await {
Ok(user) => println!("User: {}", user),
Err(ApiError::NotFound) => println!("ユーザーが見つかりません"),
Err(ApiError::Unauthorized) => println!("認証が必要です"),
Err(ApiError::ServerError(msg)) => println!("サーバーエラー: {}", msg),
Err(ApiError::NetworkError(e)) => println!("ネットワークエラー: {}", e),
}
}
3. ミドルウェアの実装
use surf::middleware::{Middleware, Next};
use surf::{Client, Request, Response};
use std::time::Instant;
// ロギングミドルウェア
#[derive(Debug)]
struct Logger;
#[surf::utils::async_trait]
impl Middleware for Logger {
async fn handle(
&self,
req: Request,
client: Client,
next: Next<'_>,
) -> Result<Response, http_types::Error> {
let url = req.url().clone();
let method = req.method();
println!("[{}] {} にリクエストを送信中...", method, url);
let start = Instant::now();
let res = next.run(req, client).await?;
let duration = start.elapsed();
println!(
"[{}] {} - ステータス: {} - 所要時間: {:?}",
method, url, res.status(), duration
);
Ok(res)
}
}
// 認証ミドルウェア
#[derive(Debug)]
struct AuthMiddleware {
token: String,
}
#[surf::utils::async_trait]
impl Middleware for AuthMiddleware {
async fn handle(
&self,
mut req: Request,
client: Client,
next: Next<'_>,
) -> Result<Response, http_types::Error> {
req.insert_header("Authorization", format!("Bearer {}", self.token));
next.run(req, client).await
}
}
// リトライミドルウェア
#[derive(Debug)]
struct RetryMiddleware {
max_retries: u32,
}
#[surf::utils::async_trait]
impl Middleware for RetryMiddleware {
async fn handle(
&self,
req: Request,
client: Client,
next: Next<'_>,
) -> Result<Response, http_types::Error> {
let mut retries = 0;
loop {
let req_clone = req.clone();
match next.run(req_clone, client.clone()).await {
Ok(res) if res.status().is_success() => return Ok(res),
Ok(res) if res.status().is_server_error() && retries < self.max_retries => {
retries += 1;
println!("リトライ {}/{}", retries, self.max_retries);
async_std::task::sleep(std::time::Duration::from_secs(retries as u64)).await;
}
Ok(res) => return Ok(res),
Err(e) if retries < self.max_retries => {
retries += 1;
println!("エラーによるリトライ {}/{}: {}", retries, self.max_retries, e);
async_std::task::sleep(std::time::Duration::from_secs(retries as u64)).await;
}
Err(e) => return Err(e),
}
}
}
}
#[async_std::main]
async fn main() -> surf::Result<()> {
let client = surf::client()
.with(Logger)
.with(AuthMiddleware { token: "your-api-token".to_string() })
.with(RetryMiddleware { max_retries: 3 });
let mut res = client.get("https://api.example.com/protected").await?;
println!("Response: {}", res.body_string().await?);
Ok(())
}
4. ストリーミングとファイルダウンロード
use surf;
use async_std::fs::File;
use async_std::io;
use futures::io::AsyncWriteExt;
// ファイルダウンロード(プログレス表示付き)
async fn download_file(url: &str, path: &str) -> surf::Result<()> {
let mut res = surf::get(url).await?;
let total_size = res.header("content-length")
.and_then(|h| h.as_str().parse::<u64>().ok());
let mut file = File::create(path).await?;
let mut downloaded = 0u64;
while let Some(chunk) = res.body_bytes().await {
let chunk = chunk?;
downloaded += chunk.len() as u64;
file.write_all(&chunk).await?;
if let Some(total) = total_size {
let progress = (downloaded as f64 / total as f64) * 100.0;
print!("\rダウンロード中: {:.1}%", progress);
use std::io::{self as stdio, Write};
stdio::stdout().flush().unwrap();
}
}
println!("\nダウンロード完了!");
Ok(())
}
// ストリーミングアップロード
async fn upload_file(url: &str, file_path: &str) -> surf::Result<()> {
let file = async_std::fs::File::open(file_path).await?;
let file_size = file.metadata().await?.len();
let body = surf::Body::from_reader(file, Some(file_size as usize));
let res = surf::post(url)
.header("Content-Type", "application/octet-stream")
.body(body)
.await?;
println!("アップロード完了: {}", res.status());
Ok(())
}
#[async_std::main]
async fn main() -> surf::Result<()> {
// ファイルをダウンロード
download_file(
"https://example.com/large-file.zip",
"downloaded-file.zip"
).await?;
// ファイルをアップロード
upload_file(
"https://api.example.com/upload",
"downloaded-file.zip"
).await?;
Ok(())
}
5. 並行リクエスト
use surf;
use futures::future;
#[async_std::main]
async fn main() -> surf::Result<()> {
let client = surf::client();
// 複数のリクエストを並行実行
let urls = vec![
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
];
let requests = urls.iter().map(|url| {
let client = client.clone();
async move {
let mut res = client.get(*url).await?;
Ok::<_, surf::Error>((url, res.body_string().await?))
}
});
let results = future::try_join_all(requests).await?;
for (url, body) in results {
println!("URL: {} - レスポンスサイズ: {} bytes", url, body.len());
}
Ok(())
}
6. WebAssemblyでの使用
// Cargo.tomlで wasm-client 機能を有効化
// surf = { version = "2.3", default-features = false, features = ["wasm-client"] }
use wasm_bindgen::prelude::*;
use surf;
#[wasm_bindgen]
pub async fn fetch_data(url: String) -> Result<String, JsValue> {
let mut res = surf::get(&url)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let body = res.body_string()
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(body)
}
他のライブラリとの比較
Surf vs Reqwest
- Surf: 複数バックエンド対応、ミドルウェアシステム、async-stdベース
- Reqwest: Tokioベース、より成熟したエコシステム、同期APIも提供
Surf vs Hyper
- Surf: 高レベルAPI、使いやすさ重視
- Hyper: 低レベル、細かい制御が可能、パフォーマンス重視
Surf vs Isahc
- Surf: 抽象化されたインターフェース、ミドルウェア対応
- Isahc: curlの薄いラッパー、curlの機能に直接アクセス
ベストプラクティス
-
クライアントの再利用
// アプリケーション全体で単一のクライアントを使用 lazy_static::lazy_static! { static ref HTTP_CLIENT: surf::Client = { surf::Config::new() .set_timeout(Some(Duration::from_secs(30))) .try_into() .unwrap() }; } -
適切なエラーハンドリング
use thiserror::Error; #[derive(Error, Debug)] enum AppError { #[error("HTTPエラー: {0}")] Http(#[from] surf::Error), #[error("JSONパースエラー: {0}")] Json(#[from] serde_json::Error), } -
タイムアウトの設定
let client: Client = Config::new() .set_timeout(Some(Duration::from_secs(30))) .try_into()?;
まとめ
Surfは、Rustにおける使いやすく柔軟なHTTPクライアントライブラリです。強力なミドルウェアシステム、複数のバックエンドサポート、完全な非同期対応により、様々なユースケースに対応できます。特に、クロスプラットフォーム対応やWebAssemblyサポートが必要な場合に適しています。