通信のタイムアウト時間を設定する

Recipe ID: net-007

ネットワークリクエストが長時間応答しない場合に、処理を中断(タイムアウト)させる方法を解説します。
Tauri の plugin-http は Web 標準の Fetch API に準拠しているため、AbortController を使用してクロスプラットフォームかつ標準的な方法でタイムアウトを実装できます。

前提条件

この機能を使用するには、@tauri-apps/plugin-http プラグインが必要です。

1. プラグインのインストール

npm run tauri add http

src-tauri/capabilities/default.jsonhttp:default 権限を追加します。
Tauri v2 ではセキュリティの観点から allow: [{ "url": "*" }] のような無制限なワイルドカード指定は許可されていない場合があるため、使用する API のドメインを明示的に指定する必要があります。

以下の例では、コード例で使用する api.example.comslow-api.com を許可しています。

{
  "permissions": [
    {
      "identifier": "http:default",
      "allow": [
        { "url": "https://api.example.com/*" },
        { "url": "https://slow-api.com/*" }
      ],
      "deny": []
    }
  ]
}
注意: 開発中は便利だからと広範囲な許可をしたくなりますが、本番ビルドでは実際に使用するドメインのみを許可することが強く推奨されます。

使用方法

1. AbortController のインスタンスを作成します。
2. setTimeout で一定時間後に controller.abort() を呼び出すタイマーをセットします。
3. fetch のオプション signalcontroller.signal を渡します。
4. リクエスト完了時には clearTimeout でタイマーを解除します。
5. タイムアウトが発生すると例外がスローされるため、try...catch で補足します。

import { fetch } from '@tauri-apps/plugin-http';

async function fetchDataSafe(): Promise<void> {
    // AbortSignal.timeout() を使うと、指定時間後に自動的にアボートされるシグナルを生成できます
    const signal = AbortSignal.timeout(5000); // 5秒後にタイムアウト

    try {
        const response = await fetch('https://api.example.com', {
            signal: signal
        });
        
        if (response.ok) {
            const data = await response.json();
            console.log(data);
        }
    } catch (err: any) {
        // Tauri v2 の plugin-http ではエラーが文字列で返る場合があります
        const isAbort = err.name === 'AbortError' || err === 'TimeoutError' || err === 'Request canceled'; 

        if (isAbort) {
            console.error('Request timed out');
        } else {
            console.error('Network error:', err);
        }
    } 
}
Note: @tauri-apps/plugin-httpfetch は、タイムアウト時などに標準の Error オブジェクトではなく文字列のエラーメッセージをスローする場合があります(実装状況によります)。そのため、catch ブロックでは err の型を確認するか、メッセージ内容で判定を行う必要がある点に注意してください。

コード例

例1: タイムアウト付き Fetch ラッパー関数

タイムアウト時間を指定できる汎用的な fetch ラッパーの実装例です。

import { fetch } from '@tauri-apps/plugin-http';

/**
 * タイムアウト機能付きの fetch
 * @param {string} url - リクエストURL
 * @param {RequestInit} options - fetch のオプション
 * @param {number} timeoutMs - タイムアウト時間(ミリ秒)
 */
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs: number = 8000): Promise<Response> {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeoutMs);

    const config: RequestInit = { 
        ...options, 
        signal: controller.signal 
    };

    try {
        const response = await fetch(url, config);
        clearTimeout(id);
        return response;
    } catch (error: any) {
        clearTimeout(id);
        // エラーハンドリングの強化
        // Tauri v2 ではエラーが文字列で返る場合があるため、複数のパターンをチェック
        const isAbort = error.name === 'AbortError' || error === 'TimeoutError' || error === 'Request canceled';

        if (isAbort) {
            throw new Error(`Request timed out after ${timeoutMs}ms`);
        }
        throw error;
    }
}

// 使用例
try {
    const res = await fetchWithTimeout('https://slow-api.com', {}, 3000);
    const data = await res.json();
} catch (e: any) {
    console.warn(e.message); // Request timed out after 3000ms
}

例2: Tauri 固有オプション(connectTimeout)について

plugin-http の Rust 実装側には connectTimeout などの設定が存在しますが、JavaScript の fetch インターフェースからは現時点(v2.0)で直接設定する標準的なオプションプロパティはありません。
上記の AbortController パターンが推奨されますが、より低レイヤーでの制御が必要な場合は、Rust コマンドを作成して reqwest クライアントを直接設定・使用する方法があります。

参考: Rust でのタイムアウト設定 (src-tauri/src/lib.rs)

Rust 側で reqwest を使用するには Cargo.toml への依存関係の追加が必要です。

[dependencies]
reqwest = { version = "0.12", features = ["json"] }
use tauri::command;
use std::time::Duration;

#[command]
async fn fetch_with_rust_timeout(url: String) -> Result<String, String> {
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(5)) // 全体のタイムアウト
        .connect_timeout(Duration::from_secs(2)) // 接続確立のタイムアウト
        .build()
        .map_err(|e| e.to_string())?;

    let res = client.get(url).send().await.map_err(|e| e.to_string())?;
    res.text().await.map_err(|e| e.to_string())
}