HTTP GET リクエストを送る(Rust経由)

HTTP プラグインの fetch と Rust の reqwest で GET を送り JSON を受け取る。http:default とスコープ allow の URL の書き方、ブラウザの fetch との違い(CORS)も示す。

通信 対象: Tauri 2.x 更新日: 読了目安: 約9分 net-001
目次
  1. 前提条件
  2. どの方法で送るか
  3. HTTP プラグインを追加する
  4. allow に書く URL の規則
  5. 1. フロントエンドから実装する (TypeScript)
  6. 2. バックエンドから実装する (Rust)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

Tauri アプリから Web API を呼ぶ方法は、ブラウザ(WebView)の fetch、HTTP プラグインの fetch、Rust のコマンドで使う HTTP クライアント reqwest の 3 つです。HTTP プラグインの fetch は JS から呼びますが通信は Rust 側で行われるので、ブラウザの CORS(別オリジンへのアクセス制限)を受けません。このレシピでは GET で JSON を取得する基本と 3 つの使い分け、HTTP プラグインの権限とスコープ(送り先 URL の許可リスト)の書き方をまとめます。

前提条件

どの方法で送るか

ブラウザの fetchHTTP プラグインの fetchRust の reqwest
通信するのはWebViewRust(JS から呼ぶ)Rust(自作のコマンド)
CORS の制限受ける受けない受けない
送り先の制限サーバーの CORS 設定スコープで許可した URL自分のコードで決める
CookieWebView のものプラグイン専用の保存先既定では保持しない
向く場面CORS を許可した自前の APIJS だけで外部 API を使うAPI キーを JS に渡さない

ページのオリジンは、Windows・Android では http://tauri.localhost、macOS・Linux では tauri://localhost、tauri dev 中は http://localhost:1420 などの開発サーバーです。サーバーが開発時のオリジンだけを許可していると「ビルドしたら CORS エラー」になります。サーバーを直せないなら HTTP プラグインか reqwest を使います。

HTTP プラグインを追加する

npm run tauri add http

パッケージの追加、lib.rs への登録、capability への http:default の追加が行われます。ただし http:default が許可するのは fetch 系のコマンドだけで、送り先の URL は 1 つも許可されていません。文字列の "http:default" を、allow に URL を並べたオブジェクトに書き換えます(URL は文字列だけでも書けます)。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "http:default",
      "allow": [
        { "url": "https://jsonplaceholder.typicode.com" },
        { "url": "https://api.example.com/v1/*" },
        "http://localhost:*"
      ],
      "deny": [{ "url": "https://api.example.com/v1/admin/*" }]
    }
  ]
}

allow に書く URL の規則

URL は URL Pattern という Web 標準の書式で照合され、* がワイルドカードになります。ポートとサブドメインとパスの末尾でつまずきがちです。

書き方一致する例一致しない例
https://api.example.comそのホストの全パスhttp:// の URL、:8443 などの別ポート
https://api.example.com/v1/*/v1/users?page=2/v1(末尾の / なし)
https://api.example.com/users/users?page=2/users/1
https://*.example.coma.b.example.comexample.com 自体
http://localhost:*全ポートの localhosthttp://127.0.0.1:3000
  • クエリ(? 以降)はパターンに書かなければ何が付いても一致します(net-004)。
  • deny は allow より優先されます。メソッドは区別されません。
  • https://*(443 番ポートの全 HTTPS)も書けますが、不正なスクリプトが入り込むと任意の送り先へデータを持ち出されます。使うホストだけを並べます。

1. フロントエンドから実装する (TypeScript)

@tauri-apps/plugin-http の fetch は標準の fetch と同じ形で呼べ、Response を返します。import を忘れてもエラーにならず、ブラウザの fetch が使われる点に注意します。

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

export type Todo = { userId: number; id: number; title: string; completed: boolean };

export async function getTodo(id: number): Promise<Todo> {
  let res: Response;
  try {
    res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
      method: 'GET', // 省略しても GET
      headers: { Accept: 'application/json' },
    });
  } catch (e) {
    // 通信できなかった(スコープ外・名前解決や接続の失敗など)。e は文字列で届く
    throw new Error(`通信に失敗しました: ${String(e)}`);
  }
  if (!res.ok) {
    // 404 や 500 は例外にならず、ok が false の Response になる
    throw new Error(`HTTP ${res.status} ${res.statusText}`);
  }
  return (await res.json()) as Todo;
}

拒否されたときの値は Error ではなく文字列です。404 や 500 では拒否されず res.ok が false になるので、両方を確かめます。第 2 引数にはプラグイン独自の maxRedirections(0 でリダイレクトをたどらない)、connectTimeout(net-007)、proxy も渡せます。

2. バックエンドから実装する (Rust)

API キーを WebView に置きたくないときや、結果を Rust で加工・保存するときは reqwest を使います。通信系のレシピは 0.12 系で書いています(0.13 系との違いは net-004)。

cd src-tauri
cargo add reqwest@0.12 --features json

reqwest::Client は接続を使い回すので、setup で 1 つ作って manage() で共有します。reqwest は既定で User-Agent を送らず、それを拒否する API(GitHub の REST API など)もあるので、ここで付けておきます。async fn のコマンドなら待つ間も画面は固まりません(rust-005)。

use serde::{Deserialize, Serialize};
use tauri::Manager;

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Todo {
    user_id: u32,
    id: u32,
    title: String,
    completed: bool,
}

/// reqwest のエラーは to_string() だけでは原因が出ないので、source() をたどってつなげる
fn describe(err: &reqwest::Error) -> String {
    let mut msg = err.to_string();
    let mut source = std::error::Error::source(err);
    while let Some(cause) = source {
        msg.push_str(&format!(": {cause}"));
        source = cause.source();
    }
    msg
}

/// 送り先のホストは Rust 側で固定し、JS からは ID だけを受け取る
#[tauri::command]
async fn get_todo(client: tauri::State<'_, reqwest::Client>, id: u32) -> Result<Todo, String> {
    let url = format!("https://jsonplaceholder.typicode.com/todos/{id}");
    client
        .get(&url)
        .send()
        .await
        .and_then(|res| res.error_for_status()) // 4xx / 5xx を Err にする
        .map_err(|e| describe(&e))?
        .json::<Todo>()
        .await
        .map_err(|e| describe(&e))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_http::init()) // 1 章の fetch 用(tauri add http で追加済み)
        .setup(|app| {
            let client = reqwest::Client::builder()
                .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
                .build()?;
            app.manage(client);
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![get_todo])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

to_string() だけでは「error sending request for url (…)」としか出ず、名前解決の失敗か接続の拒否か分からないため、describe() で原因をつなげています。また、HTTP プラグインのスコープは reqwest には効きません。JS から受け取った URL をそのまま取得するコマンドは任意の送り先への抜け道になるので、ホストは Rust 側で固定します。

import { invoke } from '@tauri-apps/api/core';

type Todo = { userId: number; id: number; title: string; completed: boolean };

try {
  const todo = await invoke<Todo>('get_todo', { id: 1 });
  console.log(todo.title);
} catch (e) {
  console.error(e); // Rust の Err(String) が文字列で届く
}

動作確認

npm run tauri dev で起動し、ボタンのクリック処理などで次を実行します。

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

const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
console.log(res.status, res.headers.get('content-type'));
console.log(await res.json());
200 application/json; charset=utf-8
{userId: 1, id: 1, title: 'delectus aut autem', completed: false}

allow に無い https://example.com/ では「url not allowed on the configured scope: https://example.com/」で拒否されることも確かめておきます。通信は Rust が行うので、DevTools の Network タブには出ません。

よくあるエラーと対処法

  • 「http.fetch not allowed. Permissions associated with this command: http:allow-fetch, http:default」: capability に http:default がありません。リリースビルドでは「Command plugin:http|fetch not allowed by ACL」になります。
  • 「url not allowed on the configured scope: …」: URL が allow に一致しません。スキーム、ポート、サブドメイン、パス末尾の /* を上の表で確かめます。allow の無い "http:default" のままでもこうなります。
  • コンソールに CORS のエラーが出る: fetch を import し忘れ、ブラウザの fetch が使われています。
  • 「error sending request for url (…)」: 名前解決・接続・TLS のどこかで失敗しています。証明書は net-008 を参照します。
  • 「plugin http not found」: lib.rs に .plugin(tauri_plugin_http::init()) がありません。

OS ごとの違いと注意点

  • Origin: 上記の WebView のオリジンは、HTTP プラグインが自動で付ける Origin ヘッダーの値にもなります(net-003)。
  • Windows・Android の useHttpsScheme: true にするとオリジンが https://tauri.localhost になり、ブラウザの fetch で http:// の API を呼ぶと混在コンテンツとして止められます。HTTP プラグインは影響を受けません。
  • モバイル: HTTP プラグインは Android・iOS でも使えます。Android のエミュレーターでは localhost がエミュレーター自身なので、PC 上の API は 10.0.2.2 で呼びます。
  • 大きなデータ: ダウンロード と アップロード は別のレシピで扱います。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する