Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ requests from Tauri applications.
support
* Binary request and response bodies
* Runtime allowlist management from Rust
* Rust backend API -- make HTTP requests from Rust
through the same security pipeline as the frontend


## Installation
Expand Down Expand Up @@ -268,9 +270,69 @@ try {
See [`HttpErrorCode`](guest-js/errors.ts) for the full
list of error codes and descriptions.

### Rust Backend Requests

The plugin exposes a Rust API for making HTTP requests
from backend code through the same security pipeline
(domain allowlist, private IP blocking, redirect
validation, body size limits, retry) as the frontend.

```rust
use tauri::Manager;
use tauri_plugin_http_client::HttpClientExt;

#[tauri::command]
async fn fetch_data(
app: tauri::AppHandle,
) -> Result<String, String> {
let resp = app.http_client()
.get("https://api.example.com/data")
.header("Accept", "application/json")
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| e.to_string())?;

resp.text()
.map(|s| s.to_string())
.map_err(|e| e.to_string())
}
```

> `send()` returns
> `tauri_plugin_http_client::error::Error`, which
> provides `is_retryable()` for retry decisions and can
> be matched on specific variants (e.g.,
> `Error::DomainNotAllowed`). Response body methods like
> `text()` return standard library errors.

Available builder methods:

* `get(url)` / `post(url)` -- convenience starters
* `request(method, url)` -- arbitrary HTTP method
* `.header(key, val)` -- add a header (repeatable)
* `.body(bytes)` -- set the request body
* `.timeout(duration)` -- per-request timeout
* `.max_retries(n)` -- per-request retry cap
* `.send()` -- execute through the security pipeline

The response provides native `reqwest` types:

* `status()` -- `reqwest::StatusCode`
* `headers()` -- `&reqwest::header::HeaderMap`
* `url()` -- `&url::Url` (final URL after redirects)
* `redirected()` -- `bool`
* `body()` / `into_body()` -- `&[u8]` / `Vec<u8>`
* `text()` -- `Result<&str, std::str::Utf8Error>`
* `retry_count()` -- number of retries performed


## Security

Both the TypeScript frontend and Rust backend API share
the same security pipeline. All protections below apply
equally to both paths.

### Domain Allowlist

The allowlist has two tiers:
Expand Down
1 change: 1 addition & 0 deletions examples/tauri-app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ name = "tauri_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[dependencies]
serde = { version = "=1.0.228", features = ["derive"] }
tauri = { version = "=2.10.3", features = [] }
tauri-plugin-http-client = { path = "../../../" }

Expand Down
59 changes: 59 additions & 0 deletions examples/tauri-app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
use std::collections::HashMap;
use std::time::Duration;

use serde::Serialize;
use tauri_plugin_http_client::HttpClientExt;

/// Response shape returned to the frontend from the Rust backend request.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct BackendResponse {
status: u16,
status_text: String,
headers: HashMap<String, String>,
url: String,
redirected: bool,
body: String,
}

/// Demonstrates the Rust backend API: makes an HTTP request through the
/// plugin's security pipeline from Rust, then returns the result to the
/// frontend via IPC.
#[tauri::command]
async fn fetch_from_rust(
app: tauri::AppHandle,
url: String,
) -> Result<BackendResponse, String> {
let resp = app
.http_client()
.get(&url)
.header("Accept", "application/json")
.header("X-Requested-From", "rust-backend")
.send()
.await
.map_err(|e| e.to_string())?;

// Note: multi-value headers (e.g. Set-Cookie) are collapsed to last value.
let mut headers = HashMap::new();

for (name, value) in resp.headers() {
if let Ok(v) = value.to_str() {
headers.insert(name.as_str().to_string(), v.to_string());
}
}

let status_text = resp
.status()
.canonical_reason()
.unwrap_or("")
.to_string();

Ok(BackendResponse {
status: resp.status().as_u16(),
status_text,
headers,
url: resp.url().to_string(),
redirected: resp.redirected(),
body: resp.text().map(|s| s.to_string()).unwrap_or_default(),
})
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All @@ -10,6 +68,7 @@ pub fn run() {
.max_response_body_size(5 * 1024 * 1024)
.build(),
)
.invoke_handler(tauri::generate_handler![fetch_from_rust])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2 changes: 2 additions & 0 deletions examples/tauri-app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"version": "0.1.0",
"identifier": "com.silvermine.httpClientExample",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
Expand Down
15 changes: 15 additions & 0 deletions examples/tauri-app/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ <h2>Binary Response</h2>
</div>
</section>

<!-- Rust Backend Request Panel -->
<section class="panel">
<h2>Rust Backend Request</h2>
<p class="hint">
Invokes a Tauri command that makes an HTTP request from Rust using
<code>app.http_client().get(url).send()</code> &mdash; the same security
pipeline as the frontend, but from backend code.
</p>
<div class="controls">
<input type="text" id="rust-url" value="https://httpbin.org/get" placeholder="URL">
<button id="rust-send">Fetch from Rust</button>
</div>
<pre id="rust-output" class="output">Response will appear here...</pre>
</section>

</div>

<script type="module" src="/main.js"></script>
Expand Down
32 changes: 32 additions & 0 deletions examples/tauri-app/src/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { request, HttpHeaders, HttpClientError } from '@silvermine/tauri-plugin-http-client';
import { invoke } from '@tauri-apps/api/core';

// --- Helpers ---

Expand Down Expand Up @@ -292,3 +293,34 @@ $('binary-fetch').addEventListener('click', async function() {
btn.disabled = false;
}
});

// --- Rust Backend Request ---

$('rust-send').addEventListener('click', async function() {
const output = $('rust-output'),
btn = $('rust-send');

btn.disabled = true;
setLoading(output, 'Sending request via Rust backend');

try {
const resp = await invoke('fetch_from_rust', { url: $('rust-url').value });

setResult(output, JSON.stringify(
{
status: resp.status,
statusText: resp.statusText,
headers: resp.headers,
url: resp.url,
redirected: resp.redirected,
body: tryParseJSON(resp.body),
},
null,
2,
));
} catch(err) {
setError(output, err);
} finally {
btn.disabled = false;
}
});
Loading
Loading