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 <noreply@anthropic.com>
This commit is contained in:
Gregory SchierandClaude Opus 5.5 authored and Stetta task tk_mw563e1dcqtc4sqh committed 2026-11-10 14:42:11 -07:00
1 parent 4686d6b4a5
commit 9eac9d3da2
2 files changed
+23 -34

No files matched your search

+17 -2
View File
@@ -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<HttpResponseEventData>,
/// The jar the send starts with, so the tab can write it back with the proxy's changes.
cookie_jar: Option<CookieJar>,
/// 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<String>,
}
/// 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<JsValue> {
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<JsValue> {
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<JsValue> {
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 `{}`.
+6 -32
View File
@@ -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<string | undefined> {
try {
const version = await db.rpc<ModelVersion>("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