Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .changes/http-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
http: minor
http-js: minor
---

Added the plugin `Builder`, which sets the `userAgent` and `requestHeaders` options in Rust, plus an `on_request` hook that can change the headers of every request. `init()` is now equivalent to `Builder::new().build()`.
6 changes: 6 additions & 0 deletions .changes/http-cookie-jar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
http: minor
http-js: minor
---

Added the `HttpExt` trait. `app.http().cookie_jar()` returns the plugin's `CookieJar`, which can be passed to your own `reqwest` clients to share the webview's session, and can be cleared or saved.
10 changes: 10 additions & 0 deletions .changes/http-redirects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
http: minor
http-js: minor
---

Redirects are now followed by the plugin instead of `reqwest`, so the `requestHeaders` rules and the `on_request` hook apply again to each redirect target. Other changes to how redirects are followed:

- `Authorization`, `Cookie` and `Proxy-Authorization` headers set by the frontend are dropped once a redirect leaves the origin of the request, as the fetch spec requires. The origin includes the scheme, so this now also applies to a redirect from `http://` to `https://` on the same host, where `reqwest` used to keep them. Request such URLs over `https://` directly.
- Exceeding `maxRedirections` now fails with `too many redirects, the redirect to <url> was not followed` (the new `Error::TooManyRedirects`).
- `Error` is now `#[non_exhaustive]`, so a `match` on it needs a wildcard arm.
6 changes: 6 additions & 0 deletions .changes/http-request-headers-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
http: minor
http-js: minor
---

Added the `requestHeaders` configuration option, which sets or removes headers, including `Origin`, on requests to the URLs matching a pattern. By default a rule only replaces headers the frontend did not set; use `"mode": "override"` to always apply it.
6 changes: 6 additions & 0 deletions .changes/http-user-agent-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
http: minor
http-js: minor
---

Added the `userAgent` configuration option, which replaces the default `tauri-plugin-http/<version>` User-Agent. A User-Agent set by the frontend still takes precedence.
59 changes: 58 additions & 1 deletion examples/api/src-tauri/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::sync::Mutex;

use serde::Deserialize;
use tauri::command;
use tauri::{AppHandle, command};
use tauri_plugin_http::{
HttpExt, RequestContext,
reqwest::header::{HeaderMap, HeaderValue, ORIGIN},
};

#[derive(Debug, Deserialize)]
#[allow(unused)]
Expand All @@ -22,3 +28,54 @@ pub fn perform_request(endpoint: String, body: RequestBody) -> String {
println!("{} {:?}", endpoint, body);
"message response".into()
}

/// The URLs under `/identity/` the http plugin's `on_request` hook ran for.
static HTTP_HOOK_URLS: Mutex<Vec<String>> = Mutex::new(Vec::new());

/// The http plugin's `on_request` hook: requests to `/identity/hook/` get a header from it, and its
/// `Origin` replaces the one from the `requestHeaders` rules, which apply before it.
pub fn http_on_request(ctx: &RequestContext<'_>, headers: &mut HeaderMap) {
let path = ctx.url().path();
if path.starts_with("/identity/") {
HTTP_HOOK_URLS.lock().unwrap().push(ctx.url().to_string());
}
if path.starts_with("/identity/hook/") {
headers.insert(ORIGIN, HeaderValue::from_static("https://hook.example.com"));
if let Ok(label) = HeaderValue::from_str(ctx.webview_label()) {
headers.insert("x-hook-webview-label", label);
}
if let Some(Ok(origin)) = ctx.webview_origin().map(HeaderValue::from_str) {
headers.insert("x-hook-webview-origin", origin);
}
}
}

#[command]
pub fn http_hook_urls() -> Vec<String> {
HTTP_HOOK_URLS.lock().unwrap().clone()
}

/// Sends a request from Rust with the http plugin's cookie jar and returns the `Cookie` header the
/// example's echo server received.
#[command]
pub async fn http_native_cookie(app: AppHandle) -> Result<Option<String>, String> {
let client = tauri_plugin_http::reqwest::Client::builder()
.cookie_provider(app.http().cookie_jar())
.build()
.map_err(|e| e.to_string())?;
let response = client
.get("http://localhost:3003/native")
.send()
.await
.map_err(|e| e.to_string())?;
Ok(response
.headers()
.get("cookie")
.and_then(|value| value.to_str().ok())
.map(ToString::to_string))
}

#[command]
pub fn http_clear_cookies(app: AppHandle) {
app.http().cookie_jar().clear();
}
22 changes: 21 additions & 1 deletion examples/api/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ pub fn run() {
.plugin(tauri_plugin_biometric::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(
tauri_plugin_http::Builder::new()
.on_request(cmd::http_on_request)
.build(),
)
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
Expand Down Expand Up @@ -136,6 +140,19 @@ pub fn run() {
if let Ok(mut request) = server.recv() {
let mut body = Vec::new();
let _ = request.as_reader().read_to_end(&mut body);

// `.../redirect/<path>` redirects to `/<path>`
if let Some((_, target)) = request.url().split_once("/redirect/") {
let location = tiny_http::Header::from_bytes(
&b"Location"[..],
format!("/{target}").as_bytes(),
)
.unwrap();
let _ = request
.respond(tiny_http::Response::empty(302).with_header(location));
continue;
}

let mut headers = request.headers().to_vec();

if !headers.iter().any(|header| header.field == tiny_http::HeaderField::from_bytes(b"Cookie").unwrap()) {
Expand Down Expand Up @@ -195,6 +212,9 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
cmd::log_operation,
cmd::perform_request,
cmd::http_hook_urls,
cmd::http_native_cookie,
cmd::http_clear_cookies,
])
.build(tauri::generate_context!())
.expect("error while building tauri application");
Expand Down
24 changes: 24 additions & 0 deletions examples/api/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@
}
},
"plugins": {
"http": {
"userAgent": "tauri-api-example/1.0",
"requestHeaders": [
{
"url": "http://localhost:3003/identity/*",
"headers": {
"Origin": "https://desktop.example.com",
"X-Identity": "config"
}
},
{
"url": "http://localhost:3003/identity/override/*",
"headers": {
"User-Agent": "override/1.0",
"X-Identity": "override"
},
"mode": "override"
},
{
"url": "http://localhost:3003/identity/no-origin/*",
"headers": { "Origin": null }
}
]
},
"cli": {
"description": "Tauri API example",
"args": [
Expand Down
125 changes: 125 additions & 0 deletions examples/api/src/views/Http.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { invoke } from '@tauri-apps/api/core'
import { JsonView } from '@zerodevx/svelte-json-view'

let httpMethod = 'GET'
Expand Down Expand Up @@ -65,6 +66,83 @@
body: await response.text()
}
}

/// request identity
// the `userAgent` and `requestHeaders` rules in tauri.conf.json and the
// `on_request` hook in src-tauri/src/cmd.rs apply to these paths; the echo
// server mirrors the headers it received
const identityPaths = [
{ path: '/plain', label: 'default user agent and origin' },
{ path: '/identity/rule', label: 'rule (replace mode)' },
{ path: '/identity/override/x', label: 'rule (override mode)' },
{ path: '/identity/no-origin/x', label: 'rule removing Origin' },
{ path: '/identity/hook/x', label: 'on_request hook' }
]
const identityHeaders = [
'user-agent',
'origin',
'x-identity',
'x-hook-webview-label',
'x-hook-webview-origin'
]
let identityPath = identityPaths[0].path
let frontendUserAgent = ''
let frontendIdentity = ''
let identityResult = null

async function checkIdentity() {
const headers = {}
if (frontendUserAgent) headers['User-Agent'] = frontendUserAgent
if (frontendIdentity) headers['X-Identity'] = frontendIdentity
try {
const response = await tauriFetch(`http://localhost:3003${identityPath}`, {
headers
})
await response.text()
identityResult = Object.fromEntries(
identityHeaders.map((name) => [name, response.headers.get(name)])
)
} catch (e) {
onMessage(e)
}
}

/// cookie jar shared with Rust
let cookieResult = null

async function webviewCookieRequest() {
try {
const response = await tauriFetch('http://localhost:3003/cookies')
await response.text()
cookieResult = {
source: 'webview',
sent: response.headers.get('cookie'),
set: response.headers.get('set-cookie')
}
} catch (e) {
onMessage(e)
}
}

async function nativeCookieRequest() {
try {
cookieResult = {
source: 'rust',
sent: await invoke('http_native_cookie')
}
} catch (e) {
onMessage(e)
}
}

async function clearCookies() {
try {
await invoke('http_clear_cookies')
cookieResult = { cleared: true }
} catch (e) {
onMessage(e)
}
}
</script>

<form on:submit|preventDefault={makeHttpRequest}>
Expand Down Expand Up @@ -101,3 +179,50 @@
<br />
<br />
<JsonView json={result} />

<br />

<h3>Request identity</h3>

<div class="flex flex-col gap-2">
<select class="input" id="identity-path" bind:value={identityPath}>
{#each identityPaths as { path, label }}
<option value={path}>{path} ({label})</option>
{/each}
</select>
<input
class="input"
id="identity-user-agent"
placeholder="User-Agent set by the frontend (optional)"
bind:value={frontendUserAgent}
/>
<input
class="input"
id="identity-x-identity"
placeholder="X-Identity set by the frontend (optional)"
bind:value={frontendIdentity}
/>
<button class="btn" type="button" id="identity-send" on:click={checkIdentity}>
Send and show received headers
</button>
</div>
<br />
<JsonView json={identityResult} />

<br />

<h3>Cookie jar shared with Rust</h3>

<div class="flex flex-wrap gap-2">
<button class="btn" type="button" on:click={webviewCookieRequest}>
Webview request
</button>
<button class="btn" type="button" on:click={nativeCookieRequest}>
Rust reqwest request
</button>
<button class="btn" type="button" on:click={clearCookies}>
Clear cookie jar
</button>
</div>
<br />
<JsonView json={cookieResult} />
Loading
Loading