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
67 changes: 63 additions & 4 deletions app/utils/cloud/upstash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,62 @@ import { chunks } from "../format";
export type UpstashConfig = SyncStore["upstash"];
export type UpStashClient = ReturnType<typeof createUpstashClient>;

async function httpFetch(
url: string,
options?: RequestInit,
): Promise<Response> {
if (window.__TAURI__) {
// 转换 RequestInit 格式为 Tauri 期望的格式
const method = options?.method || "GET";
const headers: Record<string, string> = {};

// 处理 headers
if (options?.headers) {
if (options.headers instanceof Headers) {
options.headers.forEach((value, key) => {
headers[key] = value;
});
} else if (Array.isArray(options.headers)) {
options.headers.forEach(([key, value]) => {
headers[key] = value;
});
} else {
Object.assign(headers, options.headers);
}
}

// 处理 body
let body: number[] = [];
if (options?.body) {
if (typeof options.body === "string") {
body = Array.from(new TextEncoder().encode(options.body));
} else if (options.body instanceof ArrayBuffer) {
body = Array.from(new Uint8Array(options.body));
} else if (options.body instanceof Uint8Array) {
body = Array.from(options.body);
} else {
// 其他类型转换为字符串
body = Array.from(new TextEncoder().encode(String(options.body)));
}
}

const response = await window.__TAURI__.invoke("http_fetch", {
method,
url,
headers,
body,
});

// 将 Tauri 响应转换为 Response 对象格式
return new Response(new Uint8Array(response.body), {
status: response.status,
statusText: response.status_text,
headers: new Headers(response.headers),
});
}
return fetch(url, options);
}

export function createUpstashClient(store: SyncStore) {
const config = store.upstash;
const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username;
Expand All @@ -17,7 +73,7 @@ export function createUpstashClient(store: SyncStore) {
return {
async check() {
try {
const res = await fetch(this.path(`get/${storeKey}`, proxyUrl), {
const res = await httpFetch(this.path(`get/${storeKey}`, proxyUrl), {
method: "GET",
headers: this.headers(),
});
Expand All @@ -30,7 +86,7 @@ export function createUpstashClient(store: SyncStore) {
},

async redisGet(key: string) {
const res = await fetch(this.path(`get/${key}`, proxyUrl), {
const res = await httpFetch(this.path(`get/${key}`, proxyUrl), {
method: "GET",
headers: this.headers(),
});
Expand All @@ -42,7 +98,7 @@ export function createUpstashClient(store: SyncStore) {
},

async redisSet(key: string, value: string) {
const res = await fetch(this.path(`set/${key}`, proxyUrl), {
const res = await httpFetch(this.path(`set/${key}`, proxyUrl), {
method: "POST",
headers: this.headers(),
body: value,
Expand Down Expand Up @@ -81,6 +137,9 @@ export function createUpstashClient(store: SyncStore) {
};
},
path(path: string, proxyUrl: string = "") {
if (window.__TAURI__) {
return config.endpoint + "/" + path;
}
if (!path.endsWith("/")) {
path += "/";
}
Expand All @@ -96,7 +155,7 @@ export function createUpstashClient(store: SyncStore) {
const pathPrefix = "/api/upstash/";

try {
let u = new URL(proxyUrl + pathPrefix + path);
let u = new URL(proxyUrl + pathPrefix + path, window.location.origin);
// add query params
u.searchParams.append("endpoint", config.endpoint);
url = u.toString();
Expand Down
177 changes: 177 additions & 0 deletions src-tauri/src/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//
// 普通 HTTP 请求处理模块
//

use std::time::Duration;
use std::error::Error;
use std::sync::atomic::{AtomicU32, Ordering};
use std::collections::HashMap;
use reqwest::Client;
use reqwest::header::{HeaderName, HeaderMap};

static REQUEST_COUNTER: AtomicU32 = AtomicU32::new(0);

#[derive(Debug, Clone, serde::Serialize)]
pub struct FetchResponse {
request_id: u32,
status: u16,
status_text: String,
headers: HashMap<String, String>,
body: Vec<u8>,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct ErrorResponse {
request_id: u32,
status: u16,
status_text: String,
error: String,
}

#[tauri::command]
pub async fn http_fetch(
method: String,
url: String,
headers: HashMap<String, String>,
body: Vec<u8>,
) -> Result<FetchResponse, String> {

let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst);

let mut _headers = HeaderMap::new();
for (key, value) in &headers {
match key.parse::<HeaderName>() {
Ok(header_name) => {
match value.parse() {
Ok(header_value) => {
_headers.insert(header_name, header_value);
}
Err(err) => {
return Err(format!("failed to parse header value '{}': {}", value, err));
}
}
}
Err(err) => {
return Err(format!("failed to parse header name '{}': {}", key, err));
}
}
}

// 解析 HTTP 方法
let method = method.parse::<reqwest::Method>()
.map_err(|err| format!("failed to parse method: {}", err))?;

// 创建客户端
let client = Client::builder()
.default_headers(_headers)
.redirect(reqwest::redirect::Policy::limited(3))
.connect_timeout(Duration::new(10, 0))
.timeout(Duration::new(30, 0))
.build()
.map_err(|err| format!("failed to create client: {}", err))?;
Comment on lines +57 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Headers should be added to the request, not as default headers.

Setting headers as default headers on the client affects all requests made by this client instance. Since we're only making one request, headers should be set on the request builder instead.

  // Create client
  let client = Client::builder()
-    .default_headers(_headers)
    .redirect(reqwest::redirect::Policy::limited(3))
    .connect_timeout(Duration::new(10, 0))
    .timeout(Duration::new(30, 0))
    .build()
    .map_err(|err| format!("failed to create client: {}", err))?;

  // Build request
  let mut request = client.request(
    method.clone(),
    url.parse::<reqwest::Url>()
      .map_err(|err| format!("failed to parse url: {}", err))?
-  );
+  ).headers(_headers);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let client = Client::builder()
.default_headers(_headers)
.redirect(reqwest::redirect::Policy::limited(3))
.connect_timeout(Duration::new(10, 0))
.timeout(Duration::new(30, 0))
.build()
.map_err(|err| format!("failed to create client: {}", err))?;
// Create client
let client = Client::builder()
.redirect(reqwest::redirect::Policy::limited(3))
.connect_timeout(Duration::new(10, 0))
.timeout(Duration::new(30, 0))
.build()
.map_err(|err| format!("failed to create client: {}", err))?;
// Build request
let mut request = client.request(
method.clone(),
url.parse::<reqwest::Url>()
.map_err(|err| format!("failed to parse url: {}", err))?
).headers(_headers);
🤖 Prompt for AI Agents
In src-tauri/src/fetch.rs around lines 57 to 63, the code sets headers as
default headers on the Client which affects all requests; instead remove the
.default_headers(_headers) call from the Client builder and apply the headers to
the specific request before sending (use the request builder's
.headers(_headers) method or .header(key, value) calls). Ensure the headers
value is available/moved into the request builder (clone if needed) and keep the
client build without default headers, then attach the headers to the individual
GET/POST request prior to .send().


// 构建请求
let mut request = client.request(
method.clone(),
url.parse::<reqwest::Url>()
.map_err(|err| format!("failed to parse url: {}", err))?
);

// 对于需要 body 的请求方法,添加请求体
if method == reqwest::Method::POST
|| method == reqwest::Method::PUT
|| method == reqwest::Method::PATCH
|| method == reqwest::Method::DELETE {
if !body.is_empty() {
let body_bytes = bytes::Bytes::from(body);
request = request.body(body_bytes);
}
}
Comment on lines +73 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

DELETE requests typically don't have a body.

According to HTTP specifications, DELETE requests should not have a request body. While some servers may accept it, many will reject DELETE requests with bodies. Consider removing DELETE from this list or making it configurable.

  // For request methods that need a body, add the request body
  if method == reqwest::Method::POST 
    || method == reqwest::Method::PUT 
-    || method == reqwest::Method::PATCH 
-    || method == reqwest::Method::DELETE {
+    || method == reqwest::Method::PATCH {
    if !body.is_empty() {
      let body_bytes = bytes::Bytes::from(body);
      request = request.body(body_bytes);
    }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if method == reqwest::Method::POST
|| method == reqwest::Method::PUT
|| method == reqwest::Method::PATCH
|| method == reqwest::Method::DELETE {
if !body.is_empty() {
let body_bytes = bytes::Bytes::from(body);
request = request.body(body_bytes);
}
}
// For request methods that need a body, add the request body
if method == reqwest::Method::POST
|| method == reqwest::Method::PUT
|| method == reqwest::Method::PATCH {
if !body.is_empty() {
let body_bytes = bytes::Bytes::from(body);
request = request.body(body_bytes);
}
}
🤖 Prompt for AI Agents
In src-tauri/src/fetch.rs around lines 73 to 81, the conditional that attaches a
body to requests currently includes reqwest::Method::DELETE which is
non-conformant; remove DELETE from the list (or make it configurable) so DELETE
requests do not get a request body by default. Update the condition to only
include POST, PUT, PATCH (or check an explicit allow_body flag passed into the
function/config), and adjust any callers/tests to reflect that DELETE no longer
carries a body unless explicitly permitted via the new flag.


// 发送请求
let response = request.send().await
.map_err(|err| {
let error_msg = err.source()
.map(|e| e.to_string())
.unwrap_or_else(|| err.to_string());
format!("request failed: {}", error_msg)
})?;

// 获取响应状态和头部
let status = response.status().as_u16();
let status_text = response.status().canonical_reason()
.unwrap_or("Unknown")
.to_string();

let mut response_headers = HashMap::new();
for (name, value) in response.headers() {
response_headers.insert(
name.as_str().to_string(),
std::str::from_utf8(value.as_bytes())
.unwrap_or("<invalid utf8>")
.to_string()
);
}

// 读取响应体
let response_body = response.bytes().await
.map_err(|err| format!("failed to read response body: {}", err))?;

Ok(FetchResponse {
request_id,
status,
status_text,
headers: response_headers,
body: response_body.to_vec(),
})
}

#[tauri::command]
pub async fn http_fetch_text(
method: String,
url: String,
headers: HashMap<String, String>,
body: String,
) -> Result<String, String> {

// 将字符串 body 转换为字节
let body_bytes = body.into_bytes();

// 调用主要的 fetch 方法
let response = http_fetch(method, url, headers, body_bytes).await?;

// 将响应体转换为字符串
let response_text = String::from_utf8(response.body)
.map_err(|err| format!("failed to convert response to text: {}", err))?;

Ok(response_text)
}

#[tauri::command]
pub async fn http_fetch_json(
method: String,
url: String,
headers: HashMap<String, String>,
body: serde_json::Value,
) -> Result<serde_json::Value, String> {

// 将 JSON 转换为字符串再转换为字节
let body_string = serde_json::to_string(&body)
.map_err(|err| format!("failed to serialize JSON body: {}", err))?;
let body_bytes = body_string.into_bytes();

// 确保设置了正确的 Content-Type
let mut json_headers = headers;
if !json_headers.contains_key("content-type") && !json_headers.contains_key("Content-Type") {
json_headers.insert("Content-Type".to_string(), "application/json".to_string());
}

// 调用主要的 fetch 方法
let response = http_fetch(method, url, json_headers, body_bytes).await?;

// 将响应体解析为 JSON
let response_json: serde_json::Value = serde_json::from_slice(&response.body)
.map_err(|err| format!("failed to parse response as JSON: {}", err))?;

Ok(response_json)
}
8 changes: 7 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod stream;
mod fetch;

fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![stream::stream_fetch])
.invoke_handler(tauri::generate_handler![
stream::stream_fetch,
fetch::http_fetch,
fetch::http_fetch_text,
fetch::http_fetch_json
])
.plugin(tauri_plugin_window_state::Builder::default().build())
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down