Response Streaming (#124)

This commit is contained in:
Gregory Schier
2024-10-09 16:27:37 -07:00
committed by GitHub
parent 2ca30bcb31
commit da6baf72f5
20 changed files with 425 additions and 301 deletions

View 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;

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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, };

View File

@@ -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")?,

View File

@@ -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(),

View File

@@ -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, };