mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-25 10:51:57 +01:00
Response Streaming (#124)
This commit is contained in:
5
src-tauri/migrations/20241003134208_response-state.sql
Normal file
5
src-tauri/migrations/20241003134208_response-state.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE http_responses
|
||||
ADD COLUMN state TEXT DEFAULT 'closed' NOT NULL;
|
||||
|
||||
ALTER TABLE grpc_connections
|
||||
ADD COLUMN state TEXT DEFAULT 'closed' NOT NULL;
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -14,17 +11,21 @@ use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
use log::{error, warn};
|
||||
use log::{debug, error, warn};
|
||||
use mime_guess::Mime;
|
||||
use reqwest::redirect::Policy;
|
||||
use reqwest::Method;
|
||||
use reqwest::{multipart, Url};
|
||||
use reqwest::{Method, Response};
|
||||
use serde_json::Value;
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
use tokio::fs;
|
||||
use tokio::fs::{create_dir_all, File};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch::Receiver;
|
||||
use yaak_models::models::{
|
||||
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
|
||||
HttpResponseState,
|
||||
};
|
||||
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
|
||||
use yaak_plugin_runtime::events::{RenderPurpose, WindowContext};
|
||||
@@ -35,7 +36,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
response: &HttpResponse,
|
||||
environment: Option<Environment>,
|
||||
cookie_jar: Option<CookieJar>,
|
||||
cancel_rx: &mut Receiver<bool>,
|
||||
cancelled_rx: &mut Receiver<bool>,
|
||||
) -> Result<HttpResponse, String> {
|
||||
let workspace = get_workspace(window, &request.workspace_id)
|
||||
.await
|
||||
@@ -45,7 +46,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
&WindowContext::from_window(window),
|
||||
RenderPurpose::Send,
|
||||
);
|
||||
|
||||
|
||||
let rendered_request =
|
||||
render_http_request(&request, &workspace, environment.as_ref(), &cb).await;
|
||||
|
||||
@@ -114,24 +115,24 @@ pub async fn send_http_request<R: Runtime>(
|
||||
let uri = match http::Uri::from_str(url_string.as_str()) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
return response_err(
|
||||
return Ok(response_err(
|
||||
response,
|
||||
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
|
||||
window,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
};
|
||||
// Yes, we're parsing both URI and URL because they could return different errors
|
||||
let url = match Url::from_str(uri.to_string().as_str()) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
return response_err(
|
||||
return Ok(response_err(
|
||||
response,
|
||||
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
|
||||
window,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -269,12 +270,12 @@ pub async fn send_http_request<R: Runtime>(
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
match fs::read(file_path).map_err(|e| e.to_string()) {
|
||||
match fs::read(file_path).await.map_err(|e| e.to_string()) {
|
||||
Ok(f) => {
|
||||
request_builder = request_builder.body(f);
|
||||
}
|
||||
Err(e) => {
|
||||
return response_err(response, e, window).await;
|
||||
return Ok(response_err(response, e, window).await);
|
||||
}
|
||||
}
|
||||
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
||||
@@ -297,10 +298,12 @@ pub async fn send_http_request<R: Runtime>(
|
||||
let mut part = if file_path.is_empty() {
|
||||
multipart::Part::text(value.clone())
|
||||
} else {
|
||||
match fs::read(file_path.clone()) {
|
||||
match fs::read(file_path.clone()).await {
|
||||
Ok(f) => multipart::Part::bytes(f),
|
||||
Err(e) => {
|
||||
return response_err(response, e.to_string(), window).await;
|
||||
return Ok(
|
||||
response_err(response, e.to_string(), window).await
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -348,118 +351,167 @@ pub async fn send_http_request<R: Runtime>(
|
||||
let sendable_req = match request_builder.build() {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return response_err(response, e.to_string(), window).await;
|
||||
return Ok(response_err(response, e.to_string(), window).await);
|
||||
}
|
||||
};
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let (resp_tx, resp_rx) = oneshot::channel::<Result<Response, reqwest::Error>>();
|
||||
let (done_tx, done_rx) = oneshot::channel::<HttpResponse>();
|
||||
|
||||
let (resp_tx, resp_rx) = oneshot::channel();
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = resp_tx.send(client.execute(sendable_req).await);
|
||||
});
|
||||
|
||||
let raw_response = tokio::select! {
|
||||
Ok(r) = resp_rx => {r}
|
||||
_ = cancel_rx.changed() => {
|
||||
return response_err(response, "Request was cancelled".to_string(), window).await;
|
||||
Ok(r) = resp_rx => r,
|
||||
_ = cancelled_rx.changed() => {
|
||||
debug!("Request cancelled");
|
||||
return Ok(response_err(response, "Request was cancelled".to_string(), window).await);
|
||||
}
|
||||
};
|
||||
|
||||
match raw_response {
|
||||
Ok(v) => {
|
||||
let mut response = response.clone();
|
||||
response.elapsed_headers = start.elapsed().as_millis() as i32;
|
||||
let response_headers = v.headers().clone();
|
||||
response.status = v.status().as_u16() as i32;
|
||||
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
||||
response.headers = response_headers
|
||||
.iter()
|
||||
.map(|(k, v)| HttpResponseHeader {
|
||||
name: k.as_str().to_string(),
|
||||
value: v.to_str().unwrap_or_default().to_string(),
|
||||
})
|
||||
.collect();
|
||||
response.url = v.url().to_string();
|
||||
response.remote_addr = v.remote_addr().map(|a| a.to_string());
|
||||
response.version = match v.version() {
|
||||
reqwest::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
|
||||
reqwest::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
|
||||
reqwest::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
|
||||
reqwest::Version::HTTP_2 => Some("HTTP/2".to_string()),
|
||||
reqwest::Version::HTTP_3 => Some("HTTP/3".to_string()),
|
||||
_ => None,
|
||||
{
|
||||
let window = window.clone();
|
||||
let response = response.clone();
|
||||
let cancelled_rx = cancelled_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = match raw_response {
|
||||
Ok(mut v) => {
|
||||
let mut response = response.clone();
|
||||
let response_headers = v.headers().clone();
|
||||
response.elapsed_headers = start.elapsed().as_millis() as i32;
|
||||
response.status = v.status().as_u16() as i32;
|
||||
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
||||
response.headers = response_headers
|
||||
.iter()
|
||||
.map(|(k, v)| HttpResponseHeader {
|
||||
name: k.as_str().to_string(),
|
||||
value: v.to_str().unwrap_or_default().to_string(),
|
||||
})
|
||||
.collect();
|
||||
response.url = v.url().to_string();
|
||||
response.remote_addr = v.remote_addr().map(|a| a.to_string());
|
||||
response.version = match v.version() {
|
||||
reqwest::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
|
||||
reqwest::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
|
||||
reqwest::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
|
||||
reqwest::Version::HTTP_2 => Some("HTTP/2".to_string()),
|
||||
reqwest::Version::HTTP_3 => Some("HTTP/3".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
let dir = window.app_handle().path().app_data_dir().unwrap();
|
||||
let base_dir = dir.join("responses");
|
||||
create_dir_all(base_dir.clone())
|
||||
.await
|
||||
.expect("Failed to create responses dir");
|
||||
let body_path = if response.id.is_empty() {
|
||||
base_dir.join(response.id.clone())
|
||||
} else {
|
||||
base_dir.join(uuid::Uuid::new_v4().to_string())
|
||||
};
|
||||
|
||||
response.body_path = Some(
|
||||
body_path
|
||||
.to_str()
|
||||
.expect("Failed to get body path")
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let content_length = v.content_length();
|
||||
response.state = HttpResponseState::Connected;
|
||||
response = update_response_if_id(&window, &response)
|
||||
.await
|
||||
.expect("Failed to update response after connected");
|
||||
|
||||
// Write body to FS
|
||||
let mut f = File::options()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&body_path)
|
||||
.await
|
||||
.expect("Failed to open file");
|
||||
|
||||
let mut written_bytes: usize = 0;
|
||||
loop {
|
||||
let chunk = v.chunk().await;
|
||||
if *cancelled_rx.borrow() {
|
||||
// Request was canceled
|
||||
return;
|
||||
}
|
||||
match chunk {
|
||||
Ok(Some(bytes)) => {
|
||||
f.write_all(&bytes).await.expect("Failed to write to file");
|
||||
f.flush().await.expect("Failed to flush file");
|
||||
written_bytes += bytes.len();
|
||||
response.elapsed = start.elapsed().as_millis() as i32;
|
||||
response.content_length = Some(written_bytes as i32);
|
||||
response = update_response_if_id(&window, &response)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
}
|
||||
Ok(None) => {
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
response = response_err(&response, e.to_string(), &window).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set final content length
|
||||
response.content_length = match content_length {
|
||||
Some(l) => Some(l as i32),
|
||||
None => Some(written_bytes as i32),
|
||||
};
|
||||
response.state = HttpResponseState::Closed;
|
||||
response = update_response_if_id(&window, &response)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
|
||||
// Add cookie store if specified
|
||||
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
|
||||
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
|
||||
// println!("RESPONSE COOKIE: {}", h.to_str().unwrap());
|
||||
// cookie_store::RawCookie::from_str(h.to_str().unwrap())
|
||||
// .expect("Failed to parse cookie")
|
||||
// });
|
||||
// store.store_response_cookies(cookies, &url);
|
||||
|
||||
let json_cookies: Vec<Cookie> = cookie_store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter_any()
|
||||
.map(|c| {
|
||||
let json_cookie =
|
||||
serde_json::to_value(&c).expect("Failed to serialize cookie");
|
||||
serde_json::from_value(json_cookie)
|
||||
.expect("Failed to deserialize cookie")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
cookie_jar.cookies = json_cookies;
|
||||
if let Err(e) = upsert_cookie_jar(&window, &cookie_jar).await {
|
||||
error!("Failed to update cookie jar: {}", e);
|
||||
};
|
||||
}
|
||||
response
|
||||
}
|
||||
Err(e) => response_err(&response, e.to_string(), &window).await,
|
||||
};
|
||||
|
||||
let content_length = v.content_length();
|
||||
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
|
||||
response.elapsed = start.elapsed().as_millis() as i32;
|
||||
done_tx.send(result.clone()).unwrap();
|
||||
});
|
||||
};
|
||||
|
||||
// Use content length if available, otherwise use body length
|
||||
response.content_length = match content_length {
|
||||
Some(l) => Some(l as i32),
|
||||
None => Some(body_bytes.len() as i32),
|
||||
};
|
||||
|
||||
{
|
||||
// Write body to FS
|
||||
let dir = window.app_handle().path().app_data_dir().unwrap();
|
||||
let base_dir = dir.join("responses");
|
||||
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
|
||||
let body_path = match response.id.is_empty() {
|
||||
false => base_dir.join(response.id.clone()),
|
||||
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
|
||||
};
|
||||
let mut f = File::options()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&body_path)
|
||||
.expect("Failed to open file");
|
||||
f.write_all(body_bytes.as_slice())
|
||||
.expect("Failed to write to file");
|
||||
response.body_path = Some(
|
||||
body_path
|
||||
.to_str()
|
||||
.expect("Failed to get body path")
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
response = update_response_if_id(window, &response)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
|
||||
// Add cookie store if specified
|
||||
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
|
||||
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
|
||||
// println!("RESPONSE COOKIE: {}", h.to_str().unwrap());
|
||||
// cookie_store::RawCookie::from_str(h.to_str().unwrap())
|
||||
// .expect("Failed to parse cookie")
|
||||
// });
|
||||
// store.store_response_cookies(cookies, &url);
|
||||
|
||||
let json_cookies: Vec<Cookie> = cookie_store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter_any()
|
||||
.map(|c| {
|
||||
let json_cookie =
|
||||
serde_json::to_value(&c).expect("Failed to serialize cookie");
|
||||
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
cookie_jar.cookies = json_cookies;
|
||||
if let Err(e) = upsert_cookie_jar(window, &cookie_jar).await {
|
||||
error!("Failed to update cookie jar: {}", e);
|
||||
};
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
Ok(tokio::select! {
|
||||
Ok(r) = done_rx => r,
|
||||
_ = cancelled_rx.changed() => {
|
||||
response_err(&response, "Request was cancelled".to_string(), &window).await
|
||||
}
|
||||
Err(e) => response_err(response, e.to_string(), window).await,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_proto(url_str: &str) -> String {
|
||||
|
||||
@@ -43,8 +43,9 @@ use crate::template_callback::PluginTemplateCallback;
|
||||
use crate::updates::{UpdateMode, YaakUpdater};
|
||||
use crate::window_menu::app_menu;
|
||||
use yaak_models::models::{
|
||||
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
|
||||
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace,
|
||||
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState,
|
||||
GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue,
|
||||
ModelType, Plugin, Settings, Workspace,
|
||||
};
|
||||
use yaak_models::queries::{
|
||||
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
|
||||
@@ -280,6 +281,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
request_id: req.id,
|
||||
status: -1,
|
||||
elapsed: 0,
|
||||
state: GrpcConnectionState::Initialized,
|
||||
url: req.url.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -335,6 +337,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
&GrpcConnection {
|
||||
elapsed: start.elapsed().as_millis() as i32,
|
||||
error: Some(err.clone()),
|
||||
state: GrpcConnectionState::Initialized,
|
||||
..conn.clone()
|
||||
},
|
||||
)
|
||||
@@ -689,6 +692,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
&GrpcConnection{
|
||||
elapsed: start.elapsed().as_millis() as i32,
|
||||
status: closed_status,
|
||||
state: GrpcConnectionState::Closed,
|
||||
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||
},
|
||||
).await.unwrap();
|
||||
@@ -708,6 +712,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
&GrpcConnection {
|
||||
elapsed: start.elapsed().as_millis() as i32,
|
||||
status: Code::Cancelled as i32,
|
||||
state: GrpcConnectionState::Closed,
|
||||
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||
},
|
||||
)
|
||||
@@ -752,7 +757,9 @@ async fn cmd_send_ephemeral_request(
|
||||
window.listen_any(
|
||||
format!("cancel_http_response_{}", response.id),
|
||||
move |_event| {
|
||||
let _ = cancel_tx.send(true);
|
||||
if let Err(e) = cancel_tx.send(true) {
|
||||
warn!("Failed to send cancel event for ephemeral request {e:?}");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1090,7 +1097,9 @@ async fn cmd_send_http_request(
|
||||
window.listen_any(
|
||||
format!("cancel_http_response_{}", response.id),
|
||||
move |_event| {
|
||||
let _ = cancel_tx.send(true);
|
||||
if let Err(e) = cancel_tx.send(true) {
|
||||
warn!("Failed to send cancel event for request {e:?}");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1129,15 +1138,15 @@ async fn response_err<R: Runtime>(
|
||||
response: &HttpResponse,
|
||||
error: String,
|
||||
w: &WebviewWindow<R>,
|
||||
) -> Result<HttpResponse, String> {
|
||||
) -> HttpResponse {
|
||||
warn!("Failed to send request: {}", error);
|
||||
let mut response = response.clone();
|
||||
response.elapsed = -1;
|
||||
response.state = HttpResponseState::Closed;
|
||||
response.error = Some(error.clone());
|
||||
response = update_response_if_id(w, &response)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
Ok(response)
|
||||
response
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -2182,13 +2191,16 @@ async fn call_frontend<T: Serialize + Clone, R: Runtime>(
|
||||
let (tx, mut rx) = tokio::sync::watch::channel(PromptTextResponse::default());
|
||||
|
||||
let event_id = window.clone().listen(reply_id, move |ev| {
|
||||
println!("GOT REPLY {ev:?}");
|
||||
let resp: PromptTextResponse = serde_json::from_str(ev.payload()).unwrap();
|
||||
_ = tx.send(resp);
|
||||
if let Err(e) = tx.send(resp) {
|
||||
warn!("Failed to prompt for text {e:?}");
|
||||
}
|
||||
});
|
||||
|
||||
// When reply shows up, unlisten to events and return
|
||||
_ = rx.changed().await;
|
||||
if let Err(e) = rx.changed().await {
|
||||
warn!("Failed to check channel changed {e:?}");
|
||||
}
|
||||
window.unlisten(event_id);
|
||||
|
||||
let foo = rx.borrow();
|
||||
|
||||
@@ -16,7 +16,9 @@ export type EnvironmentVariable = { enabled?: boolean, name: string, value: stri
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, sortPriority: number, };
|
||||
|
||||
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, trailers: { [key in string]?: string }, url: string, };
|
||||
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
|
||||
|
||||
export type GrpcConnectionState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
|
||||
|
||||
@@ -30,10 +32,12 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, url: string, version: string | null, };
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, };
|
||||
|
||||
export type KeyValue = { model: "key_value", createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||
|
||||
@@ -430,6 +430,21 @@ pub struct HttpResponseHeader {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde( rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
pub enum HttpResponseState {
|
||||
Initialized,
|
||||
Connected,
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl Default for HttpResponseState {
|
||||
fn default() -> Self {
|
||||
Self::Initialized
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
@@ -451,6 +466,7 @@ pub struct HttpResponse {
|
||||
pub remote_addr: Option<String>,
|
||||
pub status: i32,
|
||||
pub status_reason: Option<String>,
|
||||
pub state: HttpResponseState,
|
||||
pub url: String,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
@@ -475,6 +491,7 @@ pub enum HttpResponseIden {
|
||||
RemoteAddr,
|
||||
Status,
|
||||
StatusReason,
|
||||
State,
|
||||
Url,
|
||||
Version,
|
||||
}
|
||||
@@ -484,6 +501,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpResponse {
|
||||
|
||||
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
|
||||
let headers: String = r.get("headers")?;
|
||||
let state: String = r.get("state")?;
|
||||
Ok(HttpResponse {
|
||||
id: r.get("id")?,
|
||||
model: r.get("model")?,
|
||||
@@ -500,6 +518,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpResponse {
|
||||
remote_addr: r.get("remote_addr")?,
|
||||
status: r.get("status")?,
|
||||
status_reason: r.get("status_reason")?,
|
||||
state: serde_json::from_str(format!(r#""{state}""#).as_str()).unwrap(),
|
||||
body_path: r.get("body_path")?,
|
||||
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
||||
})
|
||||
@@ -598,6 +617,21 @@ impl<'s> TryFrom<&Row<'s>> for GrpcRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde( rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
pub enum GrpcConnectionState {
|
||||
Initialized,
|
||||
Connected,
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl Default for GrpcConnectionState{
|
||||
fn default() -> Self {
|
||||
Self::Initialized
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
@@ -615,6 +649,7 @@ pub struct GrpcConnection {
|
||||
pub method: String,
|
||||
pub service: String,
|
||||
pub status: i32,
|
||||
pub state: GrpcConnectionState,
|
||||
pub trailers: BTreeMap<String, String>,
|
||||
pub url: String,
|
||||
}
|
||||
@@ -634,6 +669,7 @@ pub enum GrpcConnectionIden {
|
||||
Error,
|
||||
Method,
|
||||
Service,
|
||||
State,
|
||||
Status,
|
||||
Trailers,
|
||||
Url,
|
||||
@@ -644,6 +680,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcConnection {
|
||||
|
||||
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
|
||||
let trailers: String = r.get("trailers")?;
|
||||
let state: String = r.get("state")?;
|
||||
Ok(GrpcConnection {
|
||||
id: r.get("id")?,
|
||||
model: r.get("model")?,
|
||||
@@ -654,6 +691,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcConnection {
|
||||
service: r.get("service")?,
|
||||
method: r.get("method")?,
|
||||
elapsed: r.get("elapsed")?,
|
||||
state: serde_json::from_str(format!(r#""{state}""#).as_str()).unwrap(),
|
||||
status: r.get("status")?,
|
||||
url: r.get("url")?,
|
||||
error: r.get("error")?,
|
||||
|
||||
@@ -3,9 +3,10 @@ use std::fs;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection,
|
||||
GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest,
|
||||
HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden,
|
||||
ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden,
|
||||
GrpcConnectionIden, GrpcConnectionState, GrpcEvent, GrpcEventIden, GrpcRequest,
|
||||
GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader,
|
||||
HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden,
|
||||
Settings, SettingsIden, Workspace, WorkspaceIden,
|
||||
};
|
||||
use crate::plugin::SqliteConnection;
|
||||
use log::{debug, error};
|
||||
@@ -433,7 +434,10 @@ pub async fn upsert_grpc_connection<R: Runtime>(
|
||||
) -> Result<GrpcConnection> {
|
||||
let connections =
|
||||
list_http_responses_for_request(window, connection.request_id.as_str(), None).await?;
|
||||
for c in connections.iter().skip(MAX_GRPC_CONNECTIONS_PER_REQUEST - 1) {
|
||||
for c in connections
|
||||
.iter()
|
||||
.skip(MAX_GRPC_CONNECTIONS_PER_REQUEST - 1)
|
||||
{
|
||||
debug!("Deleting old grpc connection {}", c.id);
|
||||
delete_grpc_connection(window, c.id.as_str()).await?;
|
||||
}
|
||||
@@ -455,6 +459,7 @@ pub async fn upsert_grpc_connection<R: Runtime>(
|
||||
GrpcConnectionIden::Service,
|
||||
GrpcConnectionIden::Method,
|
||||
GrpcConnectionIden::Elapsed,
|
||||
GrpcConnectionIden::State,
|
||||
GrpcConnectionIden::Status,
|
||||
GrpcConnectionIden::Error,
|
||||
GrpcConnectionIden::Trailers,
|
||||
@@ -469,6 +474,7 @@ pub async fn upsert_grpc_connection<R: Runtime>(
|
||||
connection.service.as_str().into(),
|
||||
connection.method.as_str().into(),
|
||||
connection.elapsed.into(),
|
||||
serde_json::to_value(&connection.state)?.as_str().into(),
|
||||
connection.status.into(),
|
||||
connection.error.as_ref().map(|s| s.as_str()).into(),
|
||||
serde_json::to_string(&connection.trailers)?.into(),
|
||||
@@ -1233,6 +1239,7 @@ pub async fn create_default_http_response<R: Runtime>(
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
HttpResponseState::Initialized,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
@@ -1251,6 +1258,7 @@ pub async fn create_http_response<R: Runtime>(
|
||||
elapsed: i64,
|
||||
elapsed_headers: i64,
|
||||
url: &str,
|
||||
state: HttpResponseState,
|
||||
status: i64,
|
||||
status_reason: Option<&str>,
|
||||
content_length: Option<i64>,
|
||||
@@ -1281,6 +1289,7 @@ pub async fn create_http_response<R: Runtime>(
|
||||
HttpResponseIden::Elapsed,
|
||||
HttpResponseIden::ElapsedHeaders,
|
||||
HttpResponseIden::Url,
|
||||
HttpResponseIden::State,
|
||||
HttpResponseIden::Status,
|
||||
HttpResponseIden::StatusReason,
|
||||
HttpResponseIden::ContentLength,
|
||||
@@ -1298,6 +1307,10 @@ pub async fn create_http_response<R: Runtime>(
|
||||
elapsed.into(),
|
||||
elapsed_headers.into(),
|
||||
url.into(),
|
||||
serde_json::to_value(state)?
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
status.into(),
|
||||
status_reason.into(),
|
||||
content_length.into(),
|
||||
@@ -1318,10 +1331,11 @@ pub async fn cancel_pending_grpc_connections(app: &AppHandle) -> Result<()> {
|
||||
let dbm = &*app.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
let closed = serde_json::to_value(&GrpcConnectionState::Closed)?;
|
||||
let (sql, params) = Query::update()
|
||||
.table(GrpcConnectionIden::Table)
|
||||
.value(GrpcConnectionIden::Elapsed, -1)
|
||||
.cond_where(Expr::col(GrpcConnectionIden::Elapsed).eq(0))
|
||||
.values([(GrpcConnectionIden::State, closed.as_str().into())])
|
||||
.cond_where(Expr::col(GrpcConnectionIden::State).ne(closed.as_str()))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
|
||||
db.execute(sql.as_str(), &*params.as_params())?;
|
||||
@@ -1332,13 +1346,14 @@ pub async fn cancel_pending_responses(app: &AppHandle) -> Result<()> {
|
||||
let dbm = &*app.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
let closed = serde_json::to_value(&GrpcConnectionState::Closed)?;
|
||||
let (sql, params) = Query::update()
|
||||
.table(HttpResponseIden::Table)
|
||||
.values([
|
||||
(HttpResponseIden::Elapsed, (-1i32).into()),
|
||||
(HttpResponseIden::State, closed.as_str().into()),
|
||||
(HttpResponseIden::StatusReason, "Cancelled".into()),
|
||||
])
|
||||
.cond_where(Expr::col(HttpResponseIden::Elapsed).eq(0))
|
||||
.cond_where(Expr::col(HttpResponseIden::State).ne(closed.as_str()))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
|
||||
db.execute(sql.as_str(), &*params.as_params())?;
|
||||
@@ -1352,11 +1367,11 @@ pub async fn update_response_if_id<R: Runtime>(
|
||||
if response.id.is_empty() {
|
||||
Ok(response.clone())
|
||||
} else {
|
||||
update_response(window, response).await
|
||||
update_http_response(window, response).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_response<R: Runtime>(
|
||||
pub async fn update_http_response<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
response: &HttpResponse,
|
||||
) -> Result<HttpResponse> {
|
||||
@@ -1397,6 +1412,10 @@ pub async fn update_response<R: Runtime>(
|
||||
HttpResponseIden::Version,
|
||||
response.version.as_ref().map(|s| s.as_str()).into(),
|
||||
),
|
||||
(
|
||||
HttpResponseIden::State,
|
||||
serde_json::to_value(&response.state)?.as_str().into(),
|
||||
),
|
||||
(
|
||||
HttpResponseIden::RemoteAddr,
|
||||
response.remote_addr.as_ref().map(|s| s.as_str()).into(),
|
||||
|
||||
@@ -14,10 +14,12 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, url: string, version: string | null, };
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, variables: Array<EnvironmentVariable>, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
Reference in New Issue
Block a user