Share the send settings and their timeline lines across desktop, tab and proxy

This commit is contained in:
Gregory Schier
2026-08-17 08:31:25 -07:00
parent e2ab4ee1b0
commit 8e466fc1a0
12 changed files with 142 additions and 156 deletions
+6 -6
View File
@@ -46,9 +46,9 @@ const WorkspacesWorkspaceIdRequestsRequestIdRoute =
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/workspaces/': typeof WorkspacesIndexRoute
'/workspaces': typeof WorkspacesIndexRoute
'/workspaces/$workspaceId/settings': typeof WorkspacesWorkspaceIdSettingsRoute
'/workspaces/$workspaceId/': typeof WorkspacesWorkspaceIdIndexRoute
'/workspaces/$workspaceId': typeof WorkspacesWorkspaceIdIndexRoute
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
}
export interface FileRoutesByTo {
@@ -70,9 +70,9 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/workspaces/'
| '/workspaces'
| '/workspaces/$workspaceId/settings'
| '/workspaces/$workspaceId/'
| '/workspaces/$workspaceId'
| '/workspaces/$workspaceId/requests/$requestId'
fileRoutesByTo: FileRoutesByTo
to:
@@ -110,14 +110,14 @@ declare module '@tanstack/react-router' {
'/workspaces/': {
id: '/workspaces/'
path: '/workspaces'
fullPath: '/workspaces/'
fullPath: '/workspaces'
preLoaderRoute: typeof WorkspacesIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/workspaces/$workspaceId/': {
id: '/workspaces/$workspaceId/'
path: '/workspaces/$workspaceId'
fullPath: '/workspaces/$workspaceId/'
fullPath: '/workspaces/$workspaceId'
preLoaderRoute: typeof WorkspacesWorkspaceIdIndexRouteImport
parentRoute: typeof rootRouteImport
}
+5 -4
View File
@@ -6,7 +6,7 @@
//! would write to its database is written to the reply stream instead, and the tab stores it.
use crate::guard::{DestinationPolicy, GuardedSender};
use crate::wire::{Frame, SendRequest, WireHeader};
use crate::wire::{Frame, SendRequest};
use base64::Engine;
use bytes::Bytes;
use log::{info, warn};
@@ -21,6 +21,7 @@ use yaak_http::cookies::CookieStore;
use yaak_http::sender::{HttpResponseEvent, ReqwestSender};
use yaak_http::transaction::HttpTransaction;
use yaak_http::types::{SendableHttpRequest, SendableHttpRequestOptions};
use yaak_models::models::HttpResponseHeader;
/// How many frames may sit unread by the client before body reading pauses. Backpressure, so a
/// slow tab slows the upstream read rather than filling memory.
@@ -115,7 +116,7 @@ pub async fn prepare(limits: Arc<SendLimits>, send: SendRequest) -> Result<Prepa
pub struct PreparedSend {
limits: Arc<SendLimits>,
sendable: SendableHttpRequest,
settings: crate::wire::SendSettings,
settings: yaak_models::models::HttpSendSettings,
cookies: Option<Vec<yaak_models::models::Cookie>>,
timeout: Duration,
timeout_capped: bool,
@@ -331,10 +332,10 @@ struct DoneStats {
content_length_compressed: u64,
}
fn to_wire_headers(headers: &[(String, String)]) -> Vec<WireHeader> {
fn to_wire_headers(headers: &[(String, String)]) -> Vec<HttpResponseHeader> {
headers
.iter()
.map(|(name, value)| WireHeader { name: name.clone(), value: value.clone() })
.map(|(name, value)| HttpResponseHeader { name: name.clone(), value: value.clone() })
.collect()
}
+8 -24
View File
@@ -11,7 +11,9 @@
//! back.
use serde::{Deserialize, Serialize};
use yaak_models::models::{Cookie, HttpRequest, HttpResponseEventData};
use yaak_models::models::{
Cookie, HttpRequest, HttpResponseEventData, HttpResponseHeader, HttpSendSettings,
};
/// The body of `POST /v1/http/send`.
#[derive(Deserialize, Debug)]
@@ -21,32 +23,14 @@ pub struct SendRequest {
/// rendered by the tab. The proxy builds the URL, headers and body from it exactly the way
/// the desktop does after rendering.
pub request: HttpRequest,
pub settings: SendSettings,
/// The resolved settings, values only. Where they came from is the tab's to record in
/// its timeline; the proxy only needs to obey them.
pub settings: HttpSendSettings,
/// The cookies to start with. `None` means no jar at all: nothing sent, nothing kept.
#[serde(default)]
pub cookies: Option<Vec<Cookie>>,
}
/// The resolved send settings, values only. Where they came from (request, folder, workspace)
/// is the tab's to record in its timeline; the proxy only needs to obey them.
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SendSettings {
pub validate_certificates: bool,
pub follow_redirects: bool,
/// Milliseconds. Zero or negative means "no timeout", which the proxy caps regardless.
pub timeout_ms: i64,
pub send_cookies: bool,
pub store_cookies: bool,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WireHeader {
pub name: String,
pub value: String,
}
/// One line of the reply stream. Tags are snake_case like the timeline event tags; fields are
/// camelCase like every model the tab stores.
#[derive(Serialize, Debug)]
@@ -68,9 +52,9 @@ pub enum Frame {
url: String,
remote_addr: Option<String>,
version: Option<String>,
headers: Vec<WireHeader>,
headers: Vec<HttpResponseHeader>,
/// The headers that were actually sent on the final hop, cookies and all.
request_headers: Vec<WireHeader>,
request_headers: Vec<HttpResponseHeader>,
/// `Content-Length` as declared by the server, if it declared one.
content_length: Option<u64>,
/// Milliseconds from the start of the send to the response head.
+11
View File
@@ -70,6 +70,17 @@ export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
/**
* The resolved send settings, values only: what an executor has to obey, with the sources
* (which model each came from) left behind in [`ResolvedHttpRequestSettings`]. This is what
* crosses from a tab to the send proxy, and what the proxy reads.
*/
export type HttpSendSettings = { validateCertificates: boolean, followRedirects: boolean,
/**
* Milliseconds. Zero or negative means no timeout.
*/
timeoutMs: number, sendCookies: boolean, storeCookies: boolean, };
export type HttpUrlParameter = { enabled?: boolean,
/**
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
+16
View File
@@ -304,6 +304,22 @@ export type HttpResponseHeader = { name: string; value: string };
export type HttpResponseState = "initialized" | "connected" | "closed";
/**
* The resolved send settings, values only: what an executor has to obey, with the sources
* (which model each came from) left behind in [`ResolvedHttpRequestSettings`]. This is what
* crosses from a tab to the send proxy, and what the proxy reads.
*/
export type HttpSendSettings = {
validateCertificates: boolean;
followRedirects: boolean;
/**
* Milliseconds. Zero or negative means no timeout.
*/
timeoutMs: number;
sendCookies: boolean;
storeCookies: boolean;
};
export type HttpUrlParameter = {
enabled?: boolean;
/**
+64
View File
@@ -140,6 +140,70 @@ impl Default for ResolvedHttpRequestSettings {
}
}
impl ResolvedHttpRequestSettings {
/// The `* Setting name=value` lines a send writes at the top of its timeline, sources and
/// all. Built here, once, so every host that runs a send — the desktop, the CLI, the browser
/// tab handing off to a proxy — records the same lines the same way.
pub fn timeline_events(&self) -> Vec<HttpResponseEventData> {
fn event<T>(
name: &str,
value: String,
setting: &ResolvedSetting<T>,
) -> HttpResponseEventData {
HttpResponseEventData::Setting {
name: name.to_string(),
value,
source_model: Some(setting.source_model.clone()),
source_id: setting.source_id.clone(),
source_name: setting.source_name.clone(),
}
}
let timeout = if self.request_timeout.value > 0 {
format!("{:?}", std::time::Duration::from_millis(self.request_timeout.value as u64))
} else {
"Infinity".to_string()
};
vec![
event(
"validate_certificates",
self.validate_certificates.value.to_string(),
&self.validate_certificates,
),
event("redirects", self.follow_redirects.value.to_string(), &self.follow_redirects),
event("timeout", timeout, &self.request_timeout),
event("send_cookies", self.send_cookies.value.to_string(), &self.send_cookies),
event("store_cookies", self.store_cookies.value.to_string(), &self.store_cookies),
]
}
}
/// The resolved send settings, values only: what an executor has to obey, with the sources
/// (which model each came from) left behind in [`ResolvedHttpRequestSettings`]. This is what
/// crosses from a tab to the send proxy, and what the proxy reads.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct HttpSendSettings {
pub validate_certificates: bool,
pub follow_redirects: bool,
/// Milliseconds. Zero or negative means no timeout.
pub timeout_ms: i32,
pub send_cookies: bool,
pub store_cookies: bool,
}
impl From<&ResolvedHttpRequestSettings> for HttpSendSettings {
fn from(s: &ResolvedHttpRequestSettings) -> Self {
Self {
validate_certificates: s.validate_certificates.value,
follow_redirects: s.follow_redirects.value,
timeout_ms: s.request_timeout.value,
send_cookies: s.send_cookies.value,
store_cookies: s.store_cookies.value,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
+4 -4
View File
@@ -694,22 +694,22 @@ export function __wbg_versions_215a3ab1c9d5745a(arg0) {
return ret;
}
export function __wbindgen_cast_0000000000000001(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1124, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1123, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3);
return ret;
}
export function __wbindgen_cast_0000000000000002(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 214, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 211, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4);
return ret;
}
export function __wbindgen_cast_0000000000000003(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 198, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 114, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h999df771f7987dc3);
return ret;
}
export function __wbindgen_cast_0000000000000004(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 212, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 209, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f);
return ret;
}
Binary file not shown.
+4 -56
View File
@@ -30,8 +30,7 @@ use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use yaak_models::blob_manager::{BlobManager, BodyChunk};
use yaak_models::models::{
AnyModel, CookieJar, HttpRequest, HttpResponseEvent, HttpResponseEventData,
ResolvedHttpRequestSettings, ResolvedSetting,
AnyModel, CookieJar, HttpRequest, HttpResponseEvent, HttpResponseEventData, HttpSendSettings,
};
use yaak_models::models_ops;
use yaak_models::query_manager::QueryManager;
@@ -378,17 +377,6 @@ struct PrepareHttpSendReq {
cookie_jar_id: Option<String>,
}
/// The values the proxy needs to obey. Where each came from is in `setting_events`.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PreparedSendSettings {
validate_certificates: bool,
follow_redirects: bool,
timeout_ms: i64,
send_cookies: bool,
store_cookies: bool,
}
/// Everything a send needs that lives in the database, resolved and rendered: the desktop's
/// `HttpSendInputs`, in the shape a tab hands to the proxy and keeps for itself.
#[derive(Serialize)]
@@ -397,7 +385,7 @@ struct PreparedHttpSend {
/// The request with inherited headers and authentication applied and every template
/// rendered. What the proxy sends, and what the response records as its request.
request: HttpRequest,
settings: PreparedSendSettings,
settings: HttpSendSettings,
/// The `* Setting name=value` timeline lines the desktop writes at the top of a send,
/// sources and all. The tab records them before the proxy's own events.
setting_events: Vec<HttpResponseEventData>,
@@ -490,17 +478,10 @@ pub async fn prepare_http_send(payload: JsValue) -> Result<JsValue> {
)));
}
let setting_events = setting_events(&settings);
let prepared = PreparedHttpSend {
request: rendered,
settings: PreparedSendSettings {
validate_certificates: settings.validate_certificates.value,
follow_redirects: settings.follow_redirects.value,
timeout_ms: settings.request_timeout.value as i64,
send_cookies: settings.send_cookies.value,
store_cookies: settings.store_cookies.value,
},
setting_events,
settings: HttpSendSettings::from(&settings),
setting_events: settings.timeline_events(),
cookie_jar,
};
// JSON-compatible, as `rpc` does: the tab posts this to the proxy with `JSON.stringify`,
@@ -509,39 +490,6 @@ pub async fn prepare_http_send(payload: JsValue) -> Result<JsValue> {
prepared.serialize(&serde_wasm_bindgen::Serializer::json_compatible()).map_err(js_error)
}
/// The same five `Setting` lines `crates/yaak/src/send.rs` writes at the top of every send.
fn setting_events(settings: &ResolvedHttpRequestSettings) -> Vec<HttpResponseEventData> {
fn event<T: ToString>(
name: &str,
value: String,
setting: &ResolvedSetting<T>,
) -> HttpResponseEventData {
HttpResponseEventData::Setting {
name: name.to_string(),
value,
source_model: Some(setting.source_model.clone()),
source_id: setting.source_id.clone(),
source_name: setting.source_name.clone(),
}
}
let timeout = if settings.request_timeout.value > 0 {
format!("{}ms", settings.request_timeout.value)
} else {
"Infinity".to_string()
};
vec![
event(
"validate_certificates",
settings.validate_certificates.value.to_string(),
&settings.validate_certificates,
),
event("redirects", settings.follow_redirects.value.to_string(), &settings.follow_redirects),
event("timeout", timeout, &settings.request_timeout),
event("send_cookies", settings.send_cookies.value.to_string(), &settings.send_cookies),
event("store_cookies", settings.store_cookies.value.to_string(), &settings.store_cookies),
]
}
/* -------------------------------------------------------------------------- */
/* Blobs */
/* -------------------------------------------------------------------------- */
+20 -54
View File
@@ -24,8 +24,8 @@ use yaak_http::types::{
use yaak_models::blob_manager::{BlobManager, BodyChunk};
use yaak_models::models::{
ClientCertificate, Cookie, CookieJar, DnsOverride, Environment, HttpRequest, HttpResponse,
HttpResponseEvent, HttpResponseHeader, HttpResponseState, ProxySetting, ProxySettingAuth,
ResolvedHttpRequestSettings, ResolvedSetting,
HttpResponseEvent, HttpResponseEventData, HttpResponseHeader, HttpResponseState, ProxySetting,
ProxySettingAuth, ResolvedHttpRequestSettings,
};
use yaak_models::query_manager::QueryManager;
use yaak_models::render::render_http_request;
@@ -716,36 +716,24 @@ pub async fn send_http_request<T: TemplateCallback>(
let started_at = Instant::now();
let request_started_url = sendable_request.url.clone();
send_setting_event(
&event_tx,
"validate_certificates",
resolved_settings.validate_certificates.value.to_string(),
&resolved_settings.validate_certificates,
);
send_setting_event(
&event_tx,
"redirects",
sendable_request.options.follow_redirects.to_string(),
&resolved_settings.follow_redirects,
);
send_setting_event(
&event_tx,
"timeout",
timeout_setting_value(sendable_request.options.timeout),
&resolved_settings.request_timeout,
);
send_setting_event(
&event_tx,
"send_cookies",
cookie_behavior.send_cookies.to_string(),
&resolved_settings.send_cookies,
);
send_setting_event(
&event_tx,
"store_cookies",
cookie_behavior.store_cookies.to_string(),
&resolved_settings.store_cookies,
);
for event in resolved_settings.timeline_events() {
if let HttpResponseEventData::Setting {
name,
value,
source_model,
source_id,
source_name,
} = event
{
let _ = event_tx.try_send(SenderHttpResponseEvent::Setting {
name,
value,
source_model,
source_id,
source_name,
});
}
}
let mut http_response =
match executor.send(sendable_request, event_tx, cookie_behavior.clone()).await {
@@ -1131,28 +1119,6 @@ pub fn persist_cookies_after_send(
Ok(())
}
fn send_setting_event<T>(
event_tx: &mpsc::Sender<SenderHttpResponseEvent>,
name: impl Into<String>,
value: impl Into<String>,
setting: &ResolvedSetting<T>,
) {
let _ = event_tx.try_send(SenderHttpResponseEvent::Setting {
name: name.into(),
value: value.into(),
source_model: Some(setting.source_model.clone()),
source_id: setting.source_id.clone(),
source_name: setting.source_name.clone(),
});
}
fn timeout_setting_value(timeout: Option<Duration>) -> String {
match timeout {
Some(timeout) if !timeout.is_zero() => format!("{timeout:?}"),
_ => "Infinity".to_string(),
}
}
fn proxy_setting_from_settings(proxy: Option<ProxySetting>) -> HttpConnectionProxySetting {
match proxy {
None => HttpConnectionProxySetting::System,
+2 -7
View File
@@ -12,6 +12,7 @@ import type {
HttpRequest,
HttpResponseEventData,
HttpResponseHeader,
HttpSendSettings,
} from "@yaakapp-internal/models";
/* ------------------------------- location -------------------------------- */
@@ -38,13 +39,7 @@ export function proxySendUrl(): string {
export interface ProxyRequestBody {
/** The rendered request, in the model shape (see `wire.rs` `SendRequest.request`). */
request: HttpRequest;
settings: {
validateCertificates: boolean;
followRedirects: boolean;
timeoutMs: number;
sendCookies: boolean;
storeCookies: boolean;
};
settings: HttpSendSettings;
/** The jar's cookies to start from, or `null` for no jar at all. */
cookies: Cookie[] | null;
}
+2 -1
View File
@@ -29,6 +29,7 @@ import type {
HttpRequest,
HttpResponse,
HttpResponseEventData,
HttpSendSettings,
} from "@yaakapp-internal/models";
import type { WorkerConnection } from "./connection";
import type { ProxyFrame, ProxyRequestBody, ProxySendResponse } from "./proxy";
@@ -50,7 +51,7 @@ type ResponsePatch = Partial<HttpResponse>;
/** What `prepare_http_send` (crates/yaak-web) hands back. */
interface PreparedHttpSend {
request: HttpRequest;
settings: ProxyRequestBody["settings"];
settings: HttpSendSettings;
settingEvents: HttpResponseEventData[];
cookieJar: CookieJar | null;
}