|
| 1 | +--- |
| 2 | +title: Tauri 如何避免触发 CORS |
| 3 | +date: 2025-06-01 11:39 |
| 4 | +updated: 2025-06-01T11:39:53+08:00 |
| 5 | +permalink: |
| 6 | +top: 0 |
| 7 | +comments: |
| 8 | +copyright: true |
| 9 | +tags: |
| 10 | +categories: |
| 11 | +keywords: |
| 12 | +description: |
| 13 | +--- |
| 14 | +Tauri 也是通过平台侧提供的 WebView 引擎来解析渲染,所以当然也会遇到 CORS 限制.本文讲述如何处理这个问题. |
| 15 | + |
| 16 | +1、最常见的处理方式就是服务端支持 `Access-Control-Allow-Origin` 但是此处调用的不是自己的服务器,不可能全部都处理这种请求. |
| 17 | + |
| 18 | +2、tauri V2 支持跨平台(移动端)能力. 提供了 tauri-plugin-http 插件, 为其他平台提供 `fetch/XMLHttpRequest` 函数支持, 所以猜测可以直接使用 tauri-plugin-http 提供的 `fetch/XMLHttpRequest` 替代浏览器的函数 |
| 19 | + |
| 20 | +```ts |
| 21 | +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; |
| 22 | +// once binding, invoke fetch or any rust command will cause page freeze! |
| 23 | +window.fetch = tauriFetch; |
| 24 | +``` |
| 25 | + |
| 26 | +> https://github.com/ShuttleSpace/fetcher |
| 27 | +> https://github.com/tauri-apps/plugins-workspace/issues/2728 |
| 28 | +
|
| 29 | +实际测试最新版 v2.4.4 直接在 main.ts 中替换会导致页面卡死 |
| 30 | + |
| 31 | +3、第三种方案就是拦截 `http/https/ws/wss` 请求,在 rust 侧处理,然后将响应返回到UI侧 |
| 32 | + |
| 33 | +因为问题出在 `invoke('command')` 上,不能直接拦截然后就调用 rust command.所以此处通过自定义 scheme `listenTwo` 来触发 rust 拦截 |
| 34 | + |
| 35 | +> src-tauri/src/lib.rs |
| 36 | +```rust |
| 37 | + tauri::Builder::default() |
| 38 | + .register_asynchronous_uri_scheme_protocol("listentwo", |_ctx, request, responder| { |
| 39 | + let uri = request.uri().to_string(); |
| 40 | + let origin_method = request.headers().get("origin-method") |
| 41 | + .and_then(|v| v.to_str().ok()) |
| 42 | + .unwrap_or("https"); |
| 43 | + let target_url = format!("{}:{}", origin_method, uri.replace("listentwo://", "")); |
| 44 | + trace!("[listentwo] target_url: {}", target_url); |
| 45 | + static CLIENT: once_cell::sync::OnceCell<reqwest::Client> = once_cell::sync::OnceCell::new(); |
| 46 | + let client = CLIENT.get_or_init(|| { |
| 47 | + reqwest::Client::builder() |
| 48 | + .timeout(std::time::Duration::from_secs(10)) |
| 49 | + .build() |
| 50 | + .unwrap() |
| 51 | + }); |
| 52 | + let future = async move { |
| 53 | + let method = match request.method() { |
| 54 | + &Method::GET => reqwest::Method::GET, |
| 55 | + &Method::POST => reqwest::Method::POST, |
| 56 | + &Method::PUT => reqwest::Method::PUT, |
| 57 | + &Method::DELETE => reqwest::Method::DELETE, |
| 58 | + &Method::HEAD => reqwest::Method::HEAD, |
| 59 | + &Method::OPTIONS => reqwest::Method::OPTIONS, |
| 60 | + &Method::PATCH => reqwest::Method::PATCH, |
| 61 | + _ => reqwest::Method::GET, |
| 62 | + }; |
| 63 | + let is_body_method = matches!(method, reqwest::Method::POST | reqwest::Method::PUT | reqwest::Method::PATCH); |
| 64 | + let mut request_builder = client.request(method, &target_url); |
| 65 | + if is_body_method { |
| 66 | + request_builder = request_builder.body(request.body().to_vec()); |
| 67 | + } |
| 68 | + let mut header_map = reqwest::header::HeaderMap::new(); |
| 69 | + for (k, v) in request.headers().iter() { |
| 70 | + if let (Ok(header_name), Ok(header_value)) = ( |
| 71 | + reqwest::header::HeaderName::from_bytes(k.as_str().as_bytes()), |
| 72 | + reqwest::header::HeaderValue::from_str(v.to_str().unwrap_or_default()) |
| 73 | + ) { |
| 74 | + header_map.insert(header_name, header_value); |
| 75 | + } |
| 76 | + } |
| 77 | + request_builder = request_builder.headers(header_map); |
| 78 | + match request_builder.send().await { |
| 79 | + Ok(response) => { |
| 80 | + trace!("[listentwo] [get] response status: {}", response.status()); |
| 81 | + let status = response.status(); |
| 82 | + let headers = response.headers().clone(); |
| 83 | + let bytes = response.bytes().await.unwrap(); |
| 84 | + |
| 85 | + let mut builder = http::Response::builder() |
| 86 | + .status(status) |
| 87 | + .header("Access-Control-Allow-Origin", "*") |
| 88 | + .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") |
| 89 | + .header("Access-Control-Allow-Headers", "Content-Type, origin-method"); |
| 90 | + |
| 91 | + for (key, value) in headers.iter() { |
| 92 | + if key != "access-control-allow-origin" { |
| 93 | + builder = builder.header(key, value); |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + responder.respond(builder.body(bytes.to_vec()).unwrap()) |
| 98 | + }, |
| 99 | + Err(e) => { |
| 100 | + responder.respond( |
| 101 | + http::Response::builder() |
| 102 | + .status(http::StatusCode::BAD_GATEWAY) |
| 103 | + .body(format!("代理请求错误: {}", e).into_bytes()) |
| 104 | + .unwrap() |
| 105 | + ) |
| 106 | + } |
| 107 | + } |
| 108 | + }; |
| 109 | + tauri::async_runtime::spawn(future); |
| 110 | + }) |
| 111 | + .plugin(tauri_plugin_http::init()) |
| 112 | + .plugin(tauri_plugin_opener::init()) |
| 113 | + .invoke_handler(tauri::generate_handler![greet]) |
| 114 | + .run(tauri::generate_context!()) |
| 115 | + .expect("error while running tauri application"); |
| 116 | +``` |
| 117 | + |
| 118 | +> src/main.ts |
| 119 | +```ts |
| 120 | +let originFetch = window.fetch |
| 121 | +window.fetch = async function (input, init): Promise<Response> { |
| 122 | + if (typeof input === 'string' && (input.startsWith('http') || input.startsWith('https'))) { |
| 123 | + const url = new URL(input); |
| 124 | + if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') { |
| 125 | + return originFetch(input, init); |
| 126 | + } |
| 127 | + const index = input.indexOf('://') |
| 128 | + const originMethod = input.substring(0, index) |
| 129 | + const newInit = { |
| 130 | + ...init, |
| 131 | + headers: { |
| 132 | + ...init?.headers, |
| 133 | + 'origin-method': originMethod |
| 134 | + } |
| 135 | + } |
| 136 | + try { |
| 137 | + const cacheKey = `listentwo:${input}`; |
| 138 | + if (newInit.method === 'GET') { |
| 139 | + const cached = sessionStorage.getItem(cacheKey); |
| 140 | + if (cached) { |
| 141 | + return new Response(cached, { |
| 142 | + headers: new Headers({'Content-Type': 'application/json'}) |
| 143 | + }); |
| 144 | + } |
| 145 | + } |
| 146 | + const response = await originFetch("listentwo" + input.substring(index), newInit) |
| 147 | + if (newInit.method === 'GET' && response.ok) { |
| 148 | + const data = await response.clone().text(); |
| 149 | + sessionStorage.setItem(cacheKey, data); |
| 150 | + } |
| 151 | + if (newInit.method === 'OPTIONS') { |
| 152 | + return new Response(null, { |
| 153 | + status: 204, |
| 154 | + headers: { |
| 155 | + 'Access-Control-Allow-Origin': '*', |
| 156 | + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', |
| 157 | + 'Access-Control-Allow-Headers': 'Content-Type, origin-method' |
| 158 | + } |
| 159 | + }); |
| 160 | + } |
| 161 | + return response; |
| 162 | + } catch (error) { |
| 163 | + console.error("Fetch error:", error); |
| 164 | + throw error; |
| 165 | + } |
| 166 | + } |
| 167 | + return originFetch(input, init) |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +> 按照 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)说明除了 `fetch/XMLHttpRequest` 外,还有 Web Fonts, WebGL textures, Canvas drawImage, CSS Shapes Image 等. |
| 172 | +> 这些请求也可以通过相同方式进行拦截处理. |
0 commit comments