概要
TauriのHTTPプラグインは、フロントエンドから安全にHTTPリクエストを実行するための公式プラグインである。
このプラグインは、Webブラウザ標準のfetch APIと同様のインターフェースを提供しながら、Rustバックエンドを経由してリクエストを実行するため、CORS (Cross-Origin Resource Sharing) の制約を受けないというメリットがある。
Tauri v2では、プラグインはRust crateとnpmパッケージの2層構造で提供される。
Rust側でtauri-plugin-httpクレートが実際のHTTP通信を処理し、フロントエンド側では@tauri-apps/plugin-httpパッケージがTypeScript / JavaScript APIを提供する。
セキュリティ面では、Capabilities設定によるURLスコープ制御が必須である。
許可されたドメインに対してのみリクエストを送信できるように設定することにより、悪意あるコードからの保護を実現している。
このプラグインは主に以下のユースケースで使用される。
- 外部APIからのデータ取得
- REST APIやGraphQLエンドポイントへのアクセスに使用する。
- ファイルのアップロードとダウンロード
- バイナリデータを含むリクエスト / レスポンス処理に対応する。
- 認証が必要なAPIへのアクセス
- ヘッダにトークンを含めたリクエスト送信が可能である。
- CORS制約を回避したクロスドメイン通信
- WebブラウザのCORSポリシーに影響されずにリクエストを送信できる。
インストールと設定
プラグインのインストール
HTTPプラグインをプロジェクトに追加するには、以下に示すコマンドを実行する。
# Tauri CLIを使用してプラグインを追加 npm run tauri add http
このコマンドは以下に示す処理を自動的に実行する。
- Rustクレートの追加
- src-tauri/Cargo.toml ファイルに tauri-plugin-http が追加される。
- npmパッケージのインストール
- @tauri-apps/plugin-http がインストールされる。
- Capabilities設定の生成
- デフォルトの権限設定が作成される。
手動でインストールする場合は、以下に示す手順を実行する。
# npmパッケージのインストール npm install @tauri-apps/plugin-http # Cargo.tomlへの追加 # [dependencies] # tauri-plugin-http = "2"
Rust側の設定
src-tauri/src/lib.rs または src-tauri/src/main.rs でプラグインを登録する。
// src-tauri/src/lib.rs
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Capabilities設定
HTTPリクエストを許可するURLを設定するために、Capabilitiesファイルを作成または編集する。
- ファイルの場所
- src-tauri/capabilities/default.json
{
"$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://api.example.com/**"
},
{
"url": "https://jsonplaceholder.typicode.com/**"
}
]
}
]
}
URLパターンの指定方法
URLスコープではglobパターンを使用できる。
| パターン | 説明 | マッチする例 |
|---|---|---|
| https://example.com/** | 特定ドメインの全てのパス | https://example.com/api/users |
| https://*.example.com/** | サブドメインを含む | https://api.example.com/v1 |
| https://example.com/api/v* | バージョニングされたAPI | https://example.com/api/v1 https://example.com/api/v2 |
| https://example.com/**/*.json | 特定拡張子のみ | https://example.com/data/config.json |
fetch APIの基本
基本的なGETリクエスト
HTTPプラグインの fetch 関数は、Webブラウザ標準の fetch とほぼ同じインターフェースを持つ。
// components/ApiExample.tsx
import { fetch } from '@tauri-apps/plugin-http';
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
export function ApiExample() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// ユーザ一覧を取得するGETリクエスト
async function fetchUsers() {
try {
const response = await fetch<User[]>('https://jsonplaceholder.typicode.com/users');
// レスポンスが正常かチェック
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// JSONとしてパース
const data = await response.json();
setUsers(data);
}
catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
);
}
TypeScript型定義
HTTPプラグインはTypeScriptに完全対応している。
ジェネリクスを使用してレスポンスの型を指定できる。
// types/api.ts
export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
export interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// api/client.ts
import { fetch } from '@tauri-apps/plugin-http';
import type { ApiResponse, Post } from '../types/api';
// 型安全なfetchラッパー関数
async function fetchTyped<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<T>;
}
// 使用例
async function getPosts(): Promise<Post[]> {
return fetchTyped<Post[]>('https://jsonplaceholder.typicode.com/posts');
}
リクエスト設定
HTTPメソッド
GET以外のHTTPメソッドを使用する場合は、method オプションを指定する。
// api/httpMethods.ts
import { fetch } from '@tauri-apps/plugin-http';
const BASE_URL = 'https://jsonplaceholder.typicode.com';
// POSTリクエスト : 新規作成
export async function createPost(title: string, body: string, userId: number) {
const response = await fetch(`${BASE_URL}/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
body,
userId,
}),
});
return response.json();
}
// PUTリクエスト : 全体更新
export async function updatePost(id: number, data: { title: string; body: string }) {
const response = await fetch(`${BASE_URL}/posts/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return response.json();
}
// PATCHリクエスト : 部分更新
export async function patchPost(id: number, data: Partial<{ title: string; body: string }>) {
const response = await fetch(`${BASE_URL}/posts/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return response.json();
}
// DELETEリクエスト : 削除
export async function deletePost(id: number): Promise<boolean> {
const response = await fetch(`${BASE_URL}/posts/${id}`, {
method: 'DELETE',
});
return response.ok;
}
ヘッダの設定
カスタムヘッダを設定することにより、認証トークンの送信やコンテンツタイプの指定が可能である。
// api/authClient.ts
import { fetch } from '@tauri-apps/plugin-http';
// 認証トークンを含むAPIクライアント
export class AuthApiClient {
private baseUrl: string;
private token: string | null = null;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
// トークンを設定
setToken(token: string) {
this.token = token;
}
// 共通ヘッダを生成
private getHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// トークンが設定されている場合はAuthorizationヘッダを追加
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
return headers;
}
// GETリクエスト
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'GET',
headers: this.getHeaders(),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// POSTリクエスト
async post<T>(endpoint: string, data: unknown): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
}
ボディの設定
リクエストボディは文字列、JSON、FormData等、様々な形式に対応している。
// api/upload.ts
import { fetch } from '@tauri-apps/plugin-http';
// JSONデータの送信
export async function sendJson(data: object) {
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return response.json();
}
// FormDataを使用したファイルアップロード
export async function uploadFile(file: File, metadata: { name: string; description: string }) {
const formData = new FormData();
formData.append('file', file);
formData.append('name', metadata.name);
formData.append('description', metadata.description);
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
// FormDataを使用する場合はContent-Typeを設定しない
// ブラウザが自動的にmultipart/form-dataを設定する
body: formData,
});
return response.json();
}
// テキストデータの送信
export async function sendText(text: string) {
const response = await fetch('https://api.example.com/text', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: text,
});
return response.text();
}
タイムアウト設定
signal オプション と AbortController を使用してタイムアウトを定義することができる。
// utils/timeoutFetch.ts
import { fetch, Response } from '@tauri-apps/plugin-http';
// タイムアウト付きfetch
export async function fetchWithTimeout(
url: string,
options: RequestInit & { timeout?: number } = {}
): Promise<Response> {
const { timeout = 5000, ...fetchOptions } = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
});
return response;
}
finally {
clearTimeout(timeoutId);
}
}
// 使用例
async function fetchWithRetry() {
try {
const response = await fetchWithTimeout('https://api.example.com/data', {
method: 'GET',
timeout: 10000, // 10秒でタイムアウト
});
return response.json();
}
catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.error('Request timed out');
}
throw error;
}
}
レスポンス処理
JSONのパース
response.json() メソッドでJSONレスポンスをパースする。
// api/responseHandler.ts
import { fetch } from '@tauri-apps/plugin-http';
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
export async function parseJsonResponse<T>(response: Response): Promise<T> {
// レスポンスボディをテキストとして取得
const text = await response.text();
// 空のレスポンスの場合
if (!text) {
if (response.ok) {
return {} as T;
}
throw new Error(`HTTP ${response.status}: Empty response`);
}
// JSONとしてパース
try {
const json = JSON.parse(text);
if (!response.ok) {
// APIエラーレスポンスの場合
const errorMessage = json.error || json.message || `HTTP ${response.status}`;
throw new Error(errorMessage);
}
return json as T;
}
catch (e) {
if (e instanceof SyntaxError) {
throw new Error('Invalid JSON response');
}
throw e;
}
}
ステータスコードの確認
HTTPステータスコードに基づいて処理を分岐させる。
// utils/statusHandler.ts
import { fetch, Response } from '@tauri-apps/plugin-http';
export class HttpError extends Error {
constructor(
public status: number,
public statusText: string,
public body?: string
) {
super(`HTTP ${status}: ${statusText}`);
this.name = 'HttpError';
}
}
export async function handleResponse<T>(response: Response): Promise<T> {
// ステータスコードによる分岐
if (response.status >= 200 && response.status < 300) {
// 成功レスポンス (2xx)
return response.json();
}
// エラーレスポンスの処理
const body = await response.text();
switch (response.status) {
case 400:
throw new HttpError(400, 'Bad Request', body);
case 401:
throw new HttpError(401, 'Unauthorized', body);
case 403:
throw new HttpError(403, 'Forbidden', body);
case 404:
throw new HttpError(404, 'Not Found', body);
case 500:
throw new HttpError(500, 'Internal Server Error', body);
case 502:
throw new HttpError(502, 'Bad Gateway', body);
case 503:
throw new HttpError(503, 'Service Unavailable', body);
default:
throw new HttpError(response.status, response.statusText, body);
}
}
エラーハンドリング
包括的なエラーハンドリングの定義例を示す。
// hooks/useApi.ts
import { fetch } from '@tauri-apps/plugin-http';
import { useState, useCallback } from 'react';
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
execute: () => Promise<void>;
}
export function useApi<T>(
url: string,
options?: RequestInit
): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const result = await response.json();
setData(result);
}
catch (err) {
// ネットワークエラー、タイムアウト、JSONパースエラー等
if (err instanceof Error) {
setError(err);
}
else {
setError(new Error('Unknown error occurred'));
}
}
finally {
setLoading(false);
}
}, [url, options]);
return { data, loading, error, execute };
}
// 使用例
function UserProfile({ userId }: { userId: number }) {
const { data: user, loading, error, execute } = useApi<User>(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user?.name}</h1>
<button onClick={execute}>Reload</button>
</div>
);
}
URLスコープによるアクセス制御
セキュリティの重要性
HTTPプラグインはRustバックエンドを経由してリクエストを送信するため、WebブラウザのCORS制約を受けない。
このメリットは強力だが、同時にセキュリティリスクも伴う。
URLスコープを適切に設定しない場合、アプリケーション内の悪意あるコードが任意のサーバにデータを送信できる可能性がある。
そのため、必要最小限のドメインのみを許可することが重要である。
Capabilitiesファイルの設定
src-tauri/capabilities/ ディレクトリにJSONファイルを作成して権限を設定する。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "http-capability",
"description": "HTTP client capability for API access",
"windows": ["main"],
"permissions": [
{
"identifier": "http:default",
"allow": [
{
"url": "https://api.myapp.com/v1/**"
},
{
"url": "https://cdn.myapp.com/**"
},
{
"url": "https://*.cloudfront.net/**"
}
],
"deny": [
{
"url": "https://api.myapp.com/v1/admin/**"
}
]
}
]
}
複数環境の設定
開発環境と本番環境で異なるURLを許可する場合の設定例を示す。
// src-tauri/capabilities/development.json
{
"identifier": "development",
"description": "Development environment capabilities",
"windows": ["main"],
"permissions": [
{
"identifier": "http:default",
"allow": [
{ "url": "http://localhost:*" },
{ "url": "http://127.0.0.1:*" },
{ "url": "https://dev-api.myapp.com/**" }
]
}
]
}
// src-tauri/capabilities/production.json
{
"identifier": "production",
"description": "Production environment capabilities",
"windows": ["main"],
"permissions": [
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.myapp.com/**" }
]
}
]
}
Rust側reqwestとの使い分け
フロントエンドでHTTPプラグインを使用するケース
以下に示すような場合は、HTTPプラグインの使用が適している。
- UIと密接に連携するAPI呼び出し
- コンポーネント内で直接データを取得・表示する場合
- ユーザ操作に応じたリクエスト
- ボタンクリックやフォーム送信に伴うAPI呼び出し。
- 簡易的なAPIクライアント
- 複雑なバックエンド処理が不要な場合
- React Query等の状態管理ライブラリとの連携
- フロントエンドでキャッシュや状態を管理する場合
Rust側でreqwestを使用するケース
以下に示すような場合は、Rust側で reqwest クレートを使用することが適している。
- 重い処理を伴うAPI呼び出し
- レスポンスの解析や変換にCPU集約的な処理が必要な場合
- 機密情報を含むリクエスト
- APIキーやシークレットを安全に扱う必要がある場合
- バックグラウンドでの定期実行
- ユーザ操作とは無関係に定期的にデータを同期する場合
- 複雑な認証フロー
- OAuthや複雑なトークン更新ロジックが必要な場合
Rust側でのHTTPリクエスト
- Rust側
// src-tauri/src/commands/api.rs use reqwest; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct ApiResponse<T> { pub data: T, pub status: u16, } // TauriコマンドとしてAPI呼び出しを提供 #[tauri::command] pub async fn fetch_from_backend(url: String) -> Result<String, String> { let client = reqwest::Client::new(); let response = client .get(&url) .header("Authorization", "Bearer SECRET_TOKEN") // 安全にトークンを扱える .send() .await .map_err(|e| e.to_string())?; let body = response.text().await.map_err(|e| e.to_string())?; Ok(body) }
- TypeScript側からの呼び出し
import { invoke } from '@tauri-apps/api/core'; async function fetchDataSecurely() { const result = await invoke<string>('fetch_from_backend', { url: 'https://api.example.com/secure-data', }); return JSON.parse(result); }
サンプルコード
APIクライアントクラスの定義
再利用可能なAPIクライアントクラスの定義例を示す。
// lib/apiClient.ts
import { fetch, Response } from '@tauri-apps/plugin-http';
export interface RequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
timeout?: number;
}
export class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json',
...defaultHeaders,
};
}
// ベースURLを設定
setBaseUrl(url: string): void {
this.baseUrl = url;
}
// デフォルトヘッダを更新
setDefaultHeader(key: string, value: string): void {
this.defaultHeaders[key] = value;
}
// 汎用リクエストメソッド
async request<T>(endpoint: string, config: RequestConfig = {}): Promise<T> {
const { method = 'GET', headers = {}, body, timeout } = config;
const controller = new AbortController();
const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method,
headers: { ...this.defaultHeaders, ...headers },
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
return this.handleResponse<T>(response);
}
finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
// レスポンス処理
private async handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP ${response.status}: ${errorBody}`);
}
// 空のレスポンス対応
const text = await response.text();
return text ? JSON.parse(text) : ({} as T);
}
// 便利なメソッド
get<T>(endpoint: string, config?: Omit<RequestConfig, 'method' | 'body'>): Promise<T> {
return this.request<T>(endpoint, { ...config, method: 'GET' });
}
post<T>(endpoint: string, body: unknown, config?: Omit<RequestConfig, 'method'>): Promise<T> {
return this.request<T>(endpoint, { ...config, method: 'POST', body });
}
put<T>(endpoint: string, body: unknown, config?: Omit<RequestConfig, 'method'>): Promise<T> {
return this.request<T>(endpoint, { ...config, method: 'PUT', body });
}
delete<T>(endpoint: string, config?: Omit<RequestConfig, 'method'>): Promise<T> {
return this.request<T>(endpoint, { ...config, method: 'DELETE' });
}
}
認証付きリクエスト
認証トークンを管理する拡張APIクライアントの定義例を示す。
// lib/authApiClient.ts
import { ApiClient } from './apiClient';
interface TokenPair {
accessToken: string;
refreshToken: string;
}
interface LoginResponse {
user: { id: number; name: string; email: string };
tokens: TokenPair;
}
export class AuthApiClient extends ApiClient {
private tokens: TokenPair | null = null;
constructor(baseUrl: string) {
super(baseUrl);
}
// ログイン
async login(email: string, password: string): Promise<LoginResponse> {
const response = await this.post<LoginResponse>('/auth/login', { email, password });
this.tokens = response.tokens;
this.setDefaultHeader('Authorization', `Bearer ${this.tokens.accessToken}`);
return response;
}
// ログアウト
logout(): void {
this.tokens = null;
this.setDefaultHeader('Authorization', '');
}
// トークンリフレッシュ
async refreshToken(): Promise<void> {
if (!this.tokens?.refreshToken) {
throw new Error('No refresh token available');
}
const response = await this.post<TokenPair>('/auth/refresh', {
refreshToken: this.tokens.refreshToken,
});
this.tokens = response;
this.setDefaultHeader('Authorization', `Bearer ${this.tokens.accessToken}`);
}
// 認証状態の確認
isAuthenticated(): boolean {
return this.tokens !== null;
}
}
並列リクエスト
複数のAPIエンドポイントから並列でデータを取得する定義例を示す。
// api/parallelRequests.ts
import { fetch } from '@tauri-apps/plugin-http';
interface DashboardData {
user: User;
posts: Post[];
notifications: Notification[];
}
interface User {
id: number;
name: string;
}
interface Post {
id: number;
title: string;
}
interface Notification {
id: number;
message: string;
}
// 並列リクエストでダッシュボードデータを取得
export async function fetchDashboardData(userId: number): Promise<DashboardData> {
const baseUrl = 'https://jsonplaceholder.typicode.com';
// Promise.allで並列実行
const [userResponse, postsResponse, notificationsResponse] = await Promise.all([
fetch(`${baseUrl}/users/${userId}`),
fetch(`${baseUrl}/posts?userId=${userId}`),
fetch(`${baseUrl}/comments?postId=1`), // 代用としてcommentsを使用
]);
// 全てのレスポンスをチェック
if (!userResponse.ok || !postsResponse.ok || !notificationsResponse.ok) {
throw new Error('Failed to fetch dashboard data');
}
// JSONパースも並列で実行
const [user, posts, notifications] = await Promise.all([
userResponse.json() as Promise<User>,
postsResponse.json() as Promise<Post[]>,
notificationsResponse.json() as Promise<Notification[]>,
]);
return { user, posts, notifications: notifications.slice(0, 5) };
}
// Reactコンポーネントでの使用例
import { useState, useEffect } from 'react';
function Dashboard({ userId }: { userId: number }) {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchDashboardData(userId)
.then(setData)
.catch(console.error)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>Loading dashboard...</div>;
return (
<div>
<h1>Welcome, {data?.user.name}</h1>
<h2>Recent Posts ({data?.posts.length})</h2>
<h2>Notifications ({data?.notifications.length})</h2>
</div>
);
}
トラブルシューティング
URLスコープエラー
URL not allowed エラーが発生する場合、Capabilities設定を確認する。
# エラーメッセージ例 Error: URL "https://api.example.com/data" is not allowed
- 対処法
- src-tauri/capabilities/default.json ファイルを開く。
http:defaultのallow配列に対象URLを追加する。
ネットワークエラー
ネットワーク接続エラーが発生する場合のチェックリストを示す。
- インターネット接続の確認
- 他のアプリケーションでネットワークが使用できるか確認する。
- ファイアウォール設定の確認
- Tauriアプリがファイアウォールでブロックされていないか確認する。
- プロキシ設定の確認
- 企業ネットワーク等でプロキシが必要な場合は設定を確認する。
JSONパースエラー
Unexpected token エラーが発生する場合、レスポンスの形式を確認する。
// デバッグ用 : レスポンスの内容を確認
const response = await fetch(url);
const text = await response.text();
console.log('Raw response:', text);
try {
const json = JSON.parse(text);
console.log('Parsed JSON:', json);
}
catch (e) {
console.error('JSON parse error:', e);
}
関連情報