From 19a43e37855fe708548fddbde5cfe254f5894b09 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 5 Sep 2026 21:05:34 -0700 Subject: [PATCH] feat(send): link every response to the request version that produced it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HTTP snapshots in resolve_send_inputs, the last point in the pipeline that still holds the stored request — below it the request has been resolved against its folder and workspace and then rendered, and neither is what a restore should put back. Every host reaches sending through that function, so the desktop, the CLI and plugin-triggered sends all get versions without each knowing about them. gRPC and WebSocket connect do the same at their connection upserts. snapshot_request_for_send swallows its own errors: a send is not worth failing over history that couldn't be written, and an ephemeral request has no id to version. Either way the response just has no version to offer. --- crates-tauri/yaak-app-client/src/lib.rs | 8 +++++++ crates-tauri/yaak-app-client/src/ws_ext.rs | 8 +++++++ .../yaak-models/src/queries/model_versions.rs | 19 +++++++++++++++++ crates/yaak/src/send.rs | 21 ++++++++++++++++--- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index dec37c7f..fabe1ce0 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -40,6 +40,7 @@ use yaak_models::models::{ CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, HttpRequest, HttpResponse, HttpResponseState, Workspace, }; +use yaak_models::queries::any_request::AnyRequest; use yaak_models::util::{BatchUpsertResult, ImportDestination, ImportPlan, UpdateSource}; use yaak_plugins::events::{ Color, ErrorResponse, FilterResponse, InternalEvent, InternalEventPayload, PluginContext, @@ -330,6 +331,12 @@ async fn cmd_grpc_go( let settings = app_handle.db().get_settings(); let client_cert = find_client_certificate(&request.url, &settings.client_certificates); + // Capture the stored request, not the rendered one: what a restore should + // put back is what the user typed + let version_id = app_handle + .db() + .snapshot_request_for_send(&AnyRequest::GrpcRequest(unrendered_request.clone())); + let conn = app_handle.db().upsert_grpc_connection( &GrpcConnection { workspace_id: request.workspace_id.clone(), @@ -338,6 +345,7 @@ async fn cmd_grpc_go( elapsed: 0, state: GrpcConnectionState::Initialized, url: request.url.clone(), + version_id, ..Default::default() }, &UpdateSource::from_window_label(window.label()), diff --git a/crates-tauri/yaak-app-client/src/ws_ext.rs b/crates-tauri/yaak-app-client/src/ws_ext.rs index 7ccba1fe..dae701cf 100644 --- a/crates-tauri/yaak-app-client/src/ws_ext.rs +++ b/crates-tauri/yaak-app-client/src/ws_ext.rs @@ -20,6 +20,7 @@ use yaak_models::models::{ HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent, WebsocketEventType, }; +use yaak_models::queries::any_request::AnyRequest; use yaak_models::util::UpdateSource; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose}; use yaak_plugins::template_callback::PluginTemplateCallback; @@ -169,10 +170,17 @@ pub async fn cmd_ws_connect( ) .await?; + // Capture the stored request, not the rendered one: what a restore should + // put back is what the user typed + let version_id = app_handle + .db() + .snapshot_request_for_send(&AnyRequest::WebsocketRequest(unrendered_request.clone())); + let connection = app_handle.db().upsert_websocket_connection( &WebsocketConnection { workspace_id: request.workspace_id.clone(), request_id: request_id.to_string(), + version_id, ..Default::default() }, &UpdateSource::from_window_label(window.label()), diff --git a/crates/yaak-models/src/queries/model_versions.rs b/crates/yaak-models/src/queries/model_versions.rs index 984213a6..2b5cd2dc 100644 --- a/crates/yaak-models/src/queries/model_versions.rs +++ b/crates/yaak-models/src/queries/model_versions.rs @@ -7,6 +7,7 @@ use crate::models::{ use crate::queries::any_request::AnyRequest; use crate::util::UpdateSource; use crate::versions::{apply_version_document, content_hash, version_document}; +use log::warn; use sea_query::{Expr, ExprTrait, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; @@ -68,6 +69,24 @@ impl<'a> ClientDb<'a> { self.snapshot_request(&self.get_any_request(request_id)?, reason) } + /// What every send calls: capture the request, and don't make a fuss. + /// + /// A send is not worth failing over history that couldn't be written, and + /// a request with no id is ephemeral and has nothing to version. Either way + /// the response just has no version to offer. + pub fn snapshot_request_for_send(&self, request: &AnyRequest) -> Option { + if request.id().is_empty() { + return None; + } + match self.snapshot_request(request, ModelVersionReason::Send) { + Ok(version) => Some(version.id), + Err(err) => { + warn!("Failed to snapshot request before send: {err}"); + None + } + } + } + /// Write a version's content back over the live request. /// /// Anything the live request has picked up since its last version is diff --git a/crates/yaak/src/send.rs b/crates/yaak/src/send.rs index cc99144d..12e11b31 100644 --- a/crates/yaak/src/send.rs +++ b/crates/yaak/src/send.rs @@ -24,9 +24,10 @@ use yaak_http::types::{ use yaak_models::blob_manager::{BlobManager, BodyChunk}; use yaak_models::models::{ ClientCertificate, Cookie, CookieJar, DnsOverride, Environment, HttpRequest, HttpResponse, - HttpResponseEvent, HttpResponseEventData, HttpResponseHeader, HttpResponseState, ProxySetting, - ProxySettingAuth, ResolvedHttpRequestSettings, + HttpResponseEvent, HttpResponseEventData, HttpResponseHeader, HttpResponseState, + ProxySetting, ProxySettingAuth, ResolvedHttpRequestSettings, }; +use yaak_models::queries::any_request::AnyRequest; use yaak_models::query_manager::QueryManager; use yaak_models::render::render_http_request; use yaak_models::util::{UpdateSource, generate_prefixed_id}; @@ -283,6 +284,9 @@ pub struct HttpSendInputs { /// Cookies the send starts with. The store is shared, so reading it back after the send /// returns (or fails) yields the cookies the transaction collected. pub cookie_store: Option, + /// The version holding the request's content as it was when this send was resolved, + /// which the response will point at. `None` for an ephemeral request with no id. + pub version_id: Option, } /// Where a send writes its response. Without it, the send keeps everything in memory: no @@ -434,6 +438,13 @@ pub fn resolve_send_inputs( client_certificates: settings.client_certificates, }, cookie_store: cookies.map(CookieStore::from_cookies), + // Captured here rather than deeper in the send because this is the last place that + // still holds the *stored* request: further down it has been resolved against its + // folder and workspace and then rendered, and neither of those is what a restore + // should put back. Every host reaches sending through this function — the desktop, + // the CLI, plugin-triggered sends — so every response gets a version without each + // of them remembering to ask for one. + version_id: db.snapshot_request_for_send(&AnyRequest::HttpRequest(request.clone())), }) } @@ -581,7 +592,8 @@ pub async fn send_http_request_by_id( pub async fn send_http_request( params: SendHttpRequestParams<'_, T>, ) -> Result { - let HttpSendInputs { request, environment_chain, runtime_config, cookie_store } = params.inputs; + let HttpSendInputs { request, environment_chain, runtime_config, cookie_store, version_id } = + params.inputs; let (request, auth_context_id) = request.into_parts(); let storage = params.storage; let send_options = runtime_config.send_options(); @@ -619,6 +631,7 @@ pub async fn send_http_request( let mut response = params.existing_response.unwrap_or_default(); response.request_id = request.id.clone(); response.workspace_id = request.workspace_id.clone(); + response.version_id = version_id; response.request_content_length = request_content_length; response.request_headers = sendable_request .headers @@ -1345,6 +1358,7 @@ mod tests { client_certificates: Vec::new(), }, cookie_store: Some(CookieStore::new()), + version_id: None, }, template_callback: &NoopTemplateCallback, storage: None, @@ -1414,6 +1428,7 @@ mod tests { client_certificates: Vec::new(), }, cookie_store: Some(CookieStore::new()), + version_id: None, }, template_callback: &NoopTemplateCallback, storage: None,