C.W.K.
Stream
Lesson 06 of 06 · published

When the Webview Can't Read Your Headers

~13 min · tauri, cors, webview, networking

Level 0Web Tourist
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Your frontend is still a browser. The instant it calls fetch, every browser rule you forgot about is back — including CORS."

The Trap: fetch in a Webview Is Browser fetch

It's tempting to forget, but the webview running your UI is a browser, and fetch() from your frontend obeys browser security — including CORS. A specific, maddening corner: JavaScript can only read the response headers a server has explicitly allowed via Access-Control-Expose-Headers. The request succeeds, the body is fine, but response.headers.get("X-My-Custom") returns null — not because the header isn't there, but because the browser hides it from JS unless the server opted in. This cost Cinder a debugging session: a custom header was being sent, the webview just refused to surface it.

Fix 1: Expose the Header (if you control the server)

If the server is yours, add the header name to Access-Control-Expose-Headers. Now the webview's fetch is allowed to read it. This is the correct fix when the API is under your control — it's a normal CORS configuration, exactly as you'd do for any web app hitting a cross-origin API.

Fix 2: Route Through Rust (the escape hatch)

The deeper move: don't make the HTTP call from the webview at all. Make it from Rust — with reqwest or the official tauri-plugin-http — inside a command, and return the data (or the headers you care about) to the frontend. The Rust core is not a browser; CORS does not apply to it. This both sidesteps the header problem and keeps secrets like API keys out of the webview. Whenever frontend networking fights you with CORS, ask whether that request belongs in Rust instead.

Code

The symptom and the server-side fix·text
// Server response WITHOUT exposing the header:
HTTP/1.1 200 OK
X-Request-Id: abc123
Access-Control-Allow-Origin: *

// In the webview:
const res = await fetch(url);
res.headers.get("X-Request-Id"); // → null  (header is there, JS can't see it)

// Server response WITH expose_headers:
Access-Control-Expose-Headers: X-Request-Id
// now res.headers.get("X-Request-Id") → "abc123"
Route it through the core to bypass CORS·rust
// Fix 2: make the request from Rust — no CORS, full header access.
#[tauri::command]
async fn fetch_with_id(url: String) -> Result<(String, Option<String>), String> {
    let res = reqwest::get(&url).await.map_err(|e| e.to_string())?;
    let request_id = res
        .headers()
        .get("x-request-id")
        .and_then(|v| v.to_str().ok())
        .map(String::from);
    let body = res.text().await.map_err(|e| e.to_string())?;
    Ok((body, request_id)) // headers Rust read freely, handed to the webview
}

External links

Exercise

Spin up any tiny local server that returns a custom response header (X-Demo: hi). From your Tauri frontend, fetch it and try to read that header — observe it's null without expose-headers. Then add Access-Control-Expose-Headers on the server (or write a Rust command that reads the header and returns it) and confirm you can now see the value. You'll never be surprised by this again.
Hint
Without Access-Control-Expose-Headers: X-Demo, response.headers.get('X-Demo') is null in the webview even though the header was sent. The Rust route avoids CORS entirely — reqwest reads any header.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.