From 9eac9d3da2184640c4d6f2a7739b256073e52fe8 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Nov 2026 14:42:11 -0700 Subject: [PATCH] Version the browser send from the read it was prepared from The browser host snapshotted the request through its own RPC, then prepare_http_send read the row again to build what was sent. An edit committed between the two left the response pointing at a version it never sent. prepare_http_send now versions the stored row it has just read, in the same call, and hands the id back with the prepared send; the response takes it from there. Co-Authored-By: Claude Opus 5.5 --- crates/yaak-wasm/src/lib.rs | 19 ++++++++++++++-- packages/platform/src/web/send.ts | 38 +++++-------------------------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/crates/yaak-wasm/src/lib.rs b/crates/yaak-wasm/src/lib.rs index abd67eea9..3ce2ea30c 100644 --- a/crates/yaak-wasm/src/lib.rs +++ b/crates/yaak-wasm/src/lib.rs @@ -35,6 +35,7 @@ use yaak_models::models::{ HttpSendSettings, ModelVersionReason, RequestVersionComparison, }; use yaak_models::models_ops; +use yaak_models::queries::any_request::AnyRequest; use yaak_models::query_manager::QueryManager; use yaak_models::render::render_http_request; use yaak_models::util::{ModelPayload, UpdateSource}; @@ -542,6 +543,9 @@ struct PreparedHttpSend { setting_events: Vec, /// The jar the send starts with, so the tab can write it back with the proxy's changes. cookie_jar: Option, + /// The version of the stored row this send was prepared from, taken from the same + /// read, so the response names exactly what was sent. `None` if it couldn't be written. + version_id: Option, } /// A template callback for a host with no plugins. Variables render; a function is a clear @@ -584,9 +588,19 @@ pub async fn prepare_http_send(payload: JsValue) -> Result { let req: PrepareHttpSendReq = from_js(payload)?; // Everything from the database first, then release the host borrow before rendering. - let (request, environment_chain, settings, cookie_jar) = with_host(|host| { + let (request, environment_chain, settings, cookie_jar, version_id) = with_host(|host| { let db = host.queries.connect(); let request = db.get_http_request(&req.request_id).map_err(js_error)?; + // Version this read of the row, the one being sent, rather than reading it again + // where an edit could land in between. Failing to write history fails nothing. + let version_id = host + .queries + .with_tx(|tx| { + Ok::<_, yaak_models::error::Error>( + tx.snapshot_request_for_send(&AnyRequest::HttpRequest(request.clone())), + ) + }) + .unwrap_or(None); let environment_chain = db .resolve_environments( &request.workspace_id, @@ -603,7 +617,7 @@ pub async fn prepare_http_send(payload: JsValue) -> Result { None => None, }; let request = HttpRequest { authentication_type, authentication, headers, ..request }; - Ok((request, environment_chain, settings, cookie_jar)) + Ok((request, environment_chain, settings, cookie_jar, version_id)) })?; let rendered = render_http_request( @@ -634,6 +648,7 @@ pub async fn prepare_http_send(payload: JsValue) -> Result { settings: HttpSendSettings::from(&settings), setting_events: settings.timeline_events(), cookie_jar, + version_id, }; // JSON-compatible, as `rpc` does: the tab posts this to the proxy with `JSON.stringify`, // and the default serializer's `Map` for the request body would stringify to `{}`. diff --git a/packages/platform/src/web/send.ts b/packages/platform/src/web/send.ts index 5f4ec7f40..e9046c79f 100644 --- a/packages/platform/src/web/send.ts +++ b/packages/platform/src/web/send.ts @@ -30,7 +30,6 @@ import type { HttpResponse, HttpResponseEventData, HttpSendSettings, - ModelVersion, } from "@yaakapp-internal/models"; import type { Frame, SendRequest } from "@yaakapp-internal/web"; import type { WorkerConnection } from "./connection"; @@ -55,6 +54,8 @@ interface PreparedHttpSend { settings: HttpSendSettings; settingEvents: HttpResponseEventData[]; cookieJar: CookieJar | null; + /** The version of the stored request this send was prepared from, if one was written. */ + versionId: string | null; } /** The desktop writes progress at most this often while a body streams in. */ @@ -72,13 +73,7 @@ export async function sendHttpRequest( // a failure to render or to reach the server lands in the response pane as // that response's error rather than as a toast that names no request. const workspaceId = await workspaceIdOfRequest(db, requestId); - const versionId = await snapshotRequestVersion(db, requestId); - const response = new ResponseWriter(db, { - model: "http_response", - requestId, - workspaceId, - versionId, - }); + const response = new ResponseWriter(db, { model: "http_response", requestId, workspaceId }); await response.create(); const cancel = new AbortController(); @@ -95,29 +90,6 @@ export async function sendHttpRequest( return response.current(); } -/** - * Capture what is about to be sent, so the response can offer it back later. - * The desktop does this inside its send pipeline; this host's pipeline is here, - * so this is where it goes. Versions are content-addressed, so repeated sends - * of an unchanged request all point at the same one. - */ -async function snapshotRequestVersion( - db: WorkerConnection, - requestId: string, -): Promise { - try { - const version = await db.rpc("models_snapshot_request", { - requestId, - reason: "send", - }); - return version.id; - } catch (err) { - // History is not worth failing a send over - console.warn("Failed to snapshot request version", err); - return undefined; - } -} - async function runSend( db: WorkerConnection, response: ResponseWriter, @@ -131,7 +103,9 @@ async function runSend( environmentId, cookieJarId, }); - await response.patch({ url: prepared.request.url }); + // The version comes from the same read of the request that was prepared, so the + // response can offer back exactly what it sent + await response.patch({ url: prepared.request.url, versionId: prepared.versionId }); // The first line of the timeline says what did the sending and where. A // request through a proxy shows a different origin to the server than the