Introduction to the HTTP Plugin
Understand what the Tauri HTTP plugin is, why it exists, how native requests differ from browser requests, and the security model that controls access.
The Tauri HTTP plugin lets your desktop application send HTTP requests from the Rust backend instead of from the webview. If you have ever tried to call an API from a browser and hit a CORS error, this plugin solves that problem at the root. It also gives you more control, better security, and access to system-level networking features that the browser environment simply cannot offer.
What the HTTP Plugin Is
The plugin wraps the battle-tested reqwest HTTP client in Rust and exposes it to your frontend JavaScript through Tauri's IPC bridge. On the JavaScript side, it provides a fetch function that closely mimics the standard Web Fetch API — so the code you write feels familiar, but the actual network request runs outside the browser sandbox, in the Rust process.
Under the hood, the plugin is composed of:
- A Rust crate (
tauri-plugin-http) that registers the plugin with Tauri and re-exportsreqwest. - A JavaScript package (
@tauri-apps/plugin-http) that gives you thefetchfunction to use in your React components. - A permissions system that controls exactly which URLs your app is allowed to contact.
This separation is not an implementation detail — it is the reason the plugin can bypass browser restrictions while still being safe.
Why You Need the HTTP Plugin
A standard browser fetch runs inside the webview with all the constraints that browsers enforce: same-origin policy, CORS preflight requests, and blocked headers. For many desktop applications, these constraints are not helpful; they are obstacles. Your app is not a random website — it is a trusted, installed application that the user has chosen to run.
The HTTP plugin exists because desktop apps need to:
- Call third-party APIs that do not send CORS headers.
- Access local network services that a browser would block.
- Use custom certificates or proxy configurations.
- Cancel long-running requests cleanly.
- Avoid leaking request context through the browser's rendering engine.
None of these are edge cases. They are the default for any real desktop application that talks to the world.
Not a CORS workaround — a different execution context:
The plugin does not disable CORS or weaken browser security. It moves the request out of the browser entirely. The webview never touches the network for these calls; the Rust process handles everything.
Native Requests vs Browser Requests
The most important mental shift when using this plugin is understanding where the request actually lives. The table below captures the key differences.
| Aspect | Browser fetch (webview) | HTTP Plugin fetch (Rust) |
|---|---|---|
| Execution environment | Webview JavaScript engine | Tauri Rust core |
| CORS enforcement | Strict, enforced by the browser | None — the concept does not apply |
| Cookies and storage | Shared with the webview's origin | Isolated, managed by reqwest |
| Network visibility | Sees only what the webview is allowed | Sees the system's full network stack |
| TLS certificates | Uses the browser's certificate store | Uses the operating system's trust store |
| Request cancellation | AbortController | Dedicated fetch_cancel command |
A practical example: you need to call http://localhost:8080/api/data from your React frontend. If you use the browser's built-in fetch, the request is sent from the webview, and the browser engine will block it if the local server does not send Access-Control-Allow-Origin headers. With the plugin, that same URL is fetched by the Rust backend — which never checks for CORS headers — and the response is passed back to JavaScript over IPC. The local server does not need to change at all.
No CORS configuration needed:
If your frontend code suddenly starts receiving data from a server that previously returned CORS errors, and you have switched to the plugin's fetch, this is the expected behavior. The request is now handled natively, not inside the browser.
How the Plugin Works
When you call fetch from JavaScript, the following chain of events occurs:
- The JavaScript binding serializes the request (URL, method, headers, body) into a format that Tauri's IPC layer understands.
- The serialized request travels from the webview process to the Rust core process through a secure IPC channel.
- The Rust side receives the command, deserializes it, and checks it against the permission scope you configured in
capabilities. - If the URL is allowed, a
reqwestclient makes the actual HTTP request from the Rust process. - The response (status, headers, body) is streamed back to JavaScript, where your code can read it just like a Web
Responseobject.
Because the network call happens in Rust, you also get access to ClientOptions that are impossible from a browser: connection timeouts, proxy configuration, SSL certificate override (dangerous), and a redirect policy.
Disabling SSL verification is dangerous:
The plugin exposes a danger option that can disable TLS certificate and hostname verification. Never use this in production. It exists only for testing environments where you control the network completely.
Security Model and URL Scoping
By default, the plugin allows no URLs. Even after you install and initialize the plugin, every fetch call will be rejected until you explicitly declare which URLs your application is allowed to contact.
This is done through the capability file (usually src-tauri/capabilities/default.json). You list allowed URL patterns using glob syntax, and you can also add a deny list. Plugin permissions is the configuration reference for that file.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
{
"identifier": "http:default",
"allow": [{ "url": "https://api.github.com/**" }],
"deny": [{ "url": "https://api.github.com/private/**" }]
}
]
}
A few critical points about scoping:
- Patterns are glob patterns, not regular expressions.
**matches any number of path segments,*matches one segment. - If a URL matches both an allow and a deny rule, deny wins.
- The scope applies per command. In this case,
http:defaultcovers the mainfetch, cancel, and body-reading operations. - Without the
"identifier": "http:default"line, the plugin still has no permission — the default permission set must be listed explicitly.
Forgetting the permissions block:
The most frequent beginner mistake is initializing the plugin but not adding the permission entry in the capabilities file. The JavaScript fetch call will reject with a permissions error, often without a clear stack trace. Always check capabilities first when a request silently fails.
The scoping model is not a burden; it is the safety net. If a malicious dependency in your frontend tries to exfiltrate data to an unknown server, the Rust core will deny the request before it ever touches the network.
A First Look
Before diving into full request construction, here is the minimal setup to see the plugin working. The example assumes you already have a Tauri v2 project with a React frontend.
Step 1: Register the plugin in Rust
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Step 2: Grant permission to a test URL
Add a capability file that allows requests to https://httpbin.org — a safe endpoint for testing.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
{
"identifier": "http:default",
"allow": [{ "url": "https://httpbin.org/**" }]
}
]
}
Step 3: Use the plugin in a React component
Install the JavaScript package (npm install @tauri-apps/plugin-http) and call fetch from a component. Note that we import from the plugin, not from the global scope.
import { fetch } from "@tauri-apps/plugin-http";
function App() {
const testRequest = async () => {
const response = await fetch("https://httpbin.org/get", {
method: "GET",
});
console.log("Status:", response.status); // e.g. 200
console.log("Status text:", response.statusText); // e.g. "OK"
const data = await response.json();
console.log("Response body:", data);
};
return (
<div>
<button onClick={testRequest}>Send Test Request</button>
</div>
);
}
export default App;
After clicking the button, open the developer tools console. You should see status 200 and the JSON body returned by httpbin. If you inspect the network tab of the webview, you will not see this request — because it never went through the browser. The Rust process handled it entirely.
This example is intentionally simple. In a real application you would set timeouts, handle errors, and read the body in chunks.
Common Misconceptions and Mistakes
"The plugin's fetch is just a wrapper around the browser's fetch."
It is not. It serializes the request and sends it over IPC to Rust, where reqwest executes it. This is why CORS does not apply and why the request appears outside the webview's network tab.
"I can call any URL without configuration." No. Until the URL matches an allowed pattern in the capability file, every request will be rejected. This is a design choice, not a bug.
"The plugin blocks malicious requests, so I don't need to sanitize user input." The permission system prevents outbound requests to unlisted URLs, but it does not sanitize data you send. If your app accepts user-provided URLs, you must still validate and restrict them.
"I can use the plugin's fetch from Node.js modules."
The plugin's JavaScript bindings depend on Tauri's IPC layer, which exists only inside a Tauri webview. You cannot import them in a standalone Node.js script.
Summary
The HTTP plugin gives your Tauri application the ability to make network requests from the Rust backend — bypassing CORS, browser sandbox restrictions, and the shared cookie jar of the webview. It enforces a strict URL allowlist that protects your app from accidental or malicious data leaks, and it exposes a fetch API that feels familiar but runs in a fundamentally different execution context.
If you need to call an API that the browser would block, the HTTP plugin is the correct tool. If your requests are simple and the server already sends proper CORS headers, the browser's native fetch may be sufficient. The difference is not about preference; it is about whether the network call belongs to the web page or to the application itself.