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の機能に直接アクセス

ベストプラクティス

  1. クライアントの再利用

    // アプリケーション全体で単一のクライアントを使用
    lazy_static::lazy_static! {
        static ref HTTP_CLIENT: surf::Client = {
            surf::Config::new()
                .set_timeout(Some(Duration::from_secs(30)))
                .try_into()
                .unwrap()
        };
    }
    
  2. 適切なエラーハンドリング

    use thiserror::Error;
    
    #[derive(Error, Debug)]
    enum AppError {
        #[error("HTTPエラー: {0}")]
        Http(#[from] surf::Error),
        #[error("JSONパースエラー: {0}")]
        Json(#[from] serde_json::Error),
    }
    
  3. タイムアウトの設定

    let client: Client = Config::new()
        .set_timeout(Some(Duration::from_secs(30)))
        .try_into()?;
    

まとめ

Surfは、Rustにおける使いやすく柔軟なHTTPクライアントライブラリです。強力なミドルウェアシステム、複数のバックエンドサポート、完全な非同期対応により、様々なユースケースに対応できます。特に、クロスプラットフォーム対応やWebAssemblyサポートが必要な場合に適しています。