feat(settings): add HTTP version as an inherited request setting (#609)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-08-27 15:01:02 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 6777003b70
commit b7b8ae5f94
18 changed files with 958 additions and 213 deletions
+16 -1
View File
@@ -110,6 +110,7 @@ export type Folder = {
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type GraphQlIntrospection = {
@@ -214,6 +215,7 @@ export type HttpRequest = {
settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
@@ -318,6 +320,7 @@ export type HttpSendSettings = {
timeoutMs: number;
sendCookies: boolean;
storeCookies: boolean;
httpVersion: HttpVersion;
};
export type HttpUrlParameter = {
@@ -331,8 +334,12 @@ export type HttpUrlParameter = {
id?: string;
};
export type HttpVersion = "auto" | "http1" | "http2";
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
export type InheritedIntSetting = { enabled?: boolean; value: number };
export type KeyValue = {
@@ -475,7 +482,14 @@ export type WebsocketEvent = {
};
export type WebsocketEventType =
"binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text";
| "binary"
| "close"
| "error"
| "frame"
| "open"
| "ping"
| "pong"
| "text";
export type WebsocketMessageType = "text" | "binary";
@@ -522,6 +536,7 @@ export type Workspace = {
settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean;
settingStoreCookies: boolean;
settingHttpVersion: HttpVersion;
};
export type WorkspaceMeta = {
@@ -0,0 +1,5 @@
ALTER TABLE workspaces ADD COLUMN setting_http_version TEXT DEFAULT 'auto' NOT NULL;
ALTER TABLE folders ADD COLUMN setting_http_version TEXT DEFAULT '{"enabled":false,"value":"auto"}' NOT NULL;
ALTER TABLE http_requests ADD COLUMN setting_http_version TEXT DEFAULT '{"enabled":false,"value":"auto"}' NOT NULL;
+70 -3
View File
@@ -1,9 +1,9 @@
use crate::error::Result;
use crate::models::HttpRequestIden::{
Authentication, AuthenticationType, Body, BodyType, CreatedAt, Description, FolderId, Headers,
Method, Name, SettingFollowRedirects, SettingRequestTimeout, SettingSendCookies,
SettingStoreCookies, SettingValidateCertificates, SortPriority, UpdatedAt, Url, UrlParameters,
WorkspaceId,
Method, Name, SettingFollowRedirects, SettingHttpVersion, SettingRequestTimeout,
SettingSendCookies, SettingStoreCookies, SettingValidateCertificates, SortPriority, UpdatedAt,
Url, UrlParameters, WorkspaceId,
};
use crate::util::generate_prefixed_id;
use chrono::{NaiveDateTime, Utc};
@@ -143,6 +143,7 @@ pub struct ResolvedHttpRequestSettings {
pub request_message_size: ResolvedSetting<i32>,
pub send_cookies: ResolvedSetting<bool>,
pub store_cookies: ResolvedSetting<bool>,
pub http_version: ResolvedSetting<HttpVersion>,
}
impl Default for ResolvedHttpRequestSettings {
@@ -154,6 +155,7 @@ impl Default for ResolvedHttpRequestSettings {
request_message_size: ResolvedSetting::default_source(DEFAULT_REQUEST_MESSAGE_SIZE),
send_cookies: ResolvedSetting::default_source(true),
store_cookies: ResolvedSetting::default_source(true),
http_version: ResolvedSetting::default_source(HttpVersion::Auto),
}
}
}
@@ -191,6 +193,7 @@ impl ResolvedHttpRequestSettings {
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),
event("http_version", self.http_version.value.to_string(), &self.http_version),
]
}
}
@@ -208,6 +211,8 @@ pub struct HttpSendSettings {
pub timeout_ms: i32,
pub send_cookies: bool,
pub store_cookies: bool,
#[serde(default)]
pub http_version: HttpVersion,
}
impl From<&ResolvedHttpRequestSettings> for HttpSendSettings {
@@ -218,6 +223,7 @@ impl From<&ResolvedHttpRequestSettings> for HttpSendSettings {
timeout_ms: s.request_timeout.value,
send_cookies: s.send_cookies.value,
store_cookies: s.store_cookies.value,
http_version: s.http_version.value,
}
}
}
@@ -255,6 +261,49 @@ impl Default for InheritedIntSetting {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_models.ts")]
pub enum HttpVersion {
#[default]
Auto,
Http1,
Http2,
}
impl FromStr for HttpVersion {
type Err = crate::error::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"http1" => Ok(Self::Http1),
"http2" => Ok(Self::Http2),
_ => Ok(Self::Auto),
}
}
}
impl Display for HttpVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
HttpVersion::Auto => "auto",
HttpVersion::Http1 => "http1",
HttpVersion::Http2 => "http2",
};
write!(f, "{}", str)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct InheritedHttpVersionSetting {
#[serde(default)]
#[ts(optional, as = "Option<bool>")]
pub enabled: bool,
pub value: HttpVersion,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_models.ts")]
@@ -484,6 +533,7 @@ impl Default for Workspace {
setting_dns_overrides: Vec::new(),
setting_send_cookies: true,
setting_store_cookies: true,
setting_http_version: HttpVersion::Auto,
}
}
}
@@ -516,6 +566,7 @@ pub struct Workspace {
pub setting_dns_overrides: Vec<DnsOverride>,
pub setting_send_cookies: bool,
pub setting_store_cookies: bool,
pub setting_http_version: HttpVersion,
}
impl UpsertModelInfo for Workspace {
@@ -560,6 +611,7 @@ impl UpsertModelInfo for Workspace {
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
(SettingSendCookies, self.setting_send_cookies.into()),
(SettingStoreCookies, self.setting_store_cookies.into()),
(SettingHttpVersion, self.setting_http_version.to_string().into()),
])
}
@@ -579,6 +631,7 @@ impl UpsertModelInfo for Workspace {
WorkspaceIden::SettingDnsOverrides,
WorkspaceIden::SettingSendCookies,
WorkspaceIden::SettingStoreCookies,
WorkspaceIden::SettingHttpVersion,
]
}
@@ -589,6 +642,7 @@ impl UpsertModelInfo for Workspace {
let headers: String = row.get("headers")?;
let authentication: String = row.get("authentication")?;
let setting_dns_overrides: String = row.get("setting_dns_overrides")?;
let setting_http_version: String = row.get("setting_http_version")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -607,6 +661,7 @@ impl UpsertModelInfo for Workspace {
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
setting_send_cookies: row.get("setting_send_cookies")?,
setting_store_cookies: row.get("setting_store_cookies")?,
setting_http_version: setting_http_version.parse().unwrap_or_default(),
})
}
}
@@ -1078,6 +1133,7 @@ impl Default for Folder {
enabled: false,
value: DEFAULT_REQUEST_MESSAGE_SIZE,
},
setting_http_version: InheritedHttpVersionSetting::default(),
}
}
}
@@ -1108,6 +1164,7 @@ pub struct Folder {
pub setting_follow_redirects: InheritedBoolSetting,
pub setting_request_timeout: InheritedIntSetting,
pub setting_request_message_size: InheritedIntSetting,
pub setting_http_version: InheritedHttpVersionSetting,
}
impl UpsertModelInfo for Folder {
@@ -1159,6 +1216,7 @@ impl UpsertModelInfo for Folder {
SettingRequestMessageSize,
serde_json::to_string(&self.setting_request_message_size)?.into(),
),
(SettingHttpVersion, serde_json::to_string(&self.setting_http_version)?.into()),
])
}
@@ -1178,6 +1236,7 @@ impl UpsertModelInfo for Folder {
FolderIden::SettingFollowRedirects,
FolderIden::SettingRequestTimeout,
FolderIden::SettingRequestMessageSize,
FolderIden::SettingHttpVersion,
]
}
@@ -1193,6 +1252,7 @@ impl UpsertModelInfo for Folder {
let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
let setting_request_timeout: String = row.get("setting_request_timeout")?;
let setting_request_message_size: String = row.get("setting_request_message_size")?;
let setting_http_version: String = row.get("setting_http_version")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -1216,6 +1276,7 @@ impl UpsertModelInfo for Folder {
.unwrap_or_default(),
setting_request_message_size: serde_json::from_str(&setting_request_message_size)
.unwrap_or_else(|_| default_request_message_size_setting()),
setting_http_version: serde_json::from_str(&setting_http_version).unwrap_or_default(),
})
}
}
@@ -1283,6 +1344,7 @@ impl Default for HttpRequest {
setting_validate_certificates: InheritedBoolSetting::default(),
setting_follow_redirects: InheritedBoolSetting::default(),
setting_request_timeout: InheritedIntSetting::default(),
setting_http_version: InheritedHttpVersionSetting::default(),
}
}
}
@@ -1319,6 +1381,7 @@ pub struct HttpRequest {
pub setting_validate_certificates: InheritedBoolSetting,
pub setting_follow_redirects: InheritedBoolSetting,
pub setting_request_timeout: InheritedIntSetting,
pub setting_http_version: InheritedHttpVersionSetting,
}
impl UpsertModelInfo for HttpRequest {
@@ -1370,6 +1433,7 @@ impl UpsertModelInfo for HttpRequest {
),
(SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
(SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()),
(SettingHttpVersion, serde_json::to_string(&self.setting_http_version)?.into()),
])
}
@@ -1394,6 +1458,7 @@ impl UpsertModelInfo for HttpRequest {
SettingValidateCertificates,
SettingFollowRedirects,
SettingRequestTimeout,
SettingHttpVersion,
]
}
@@ -1407,6 +1472,7 @@ impl UpsertModelInfo for HttpRequest {
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
let setting_request_timeout: String = row.get("setting_request_timeout")?;
let setting_http_version: String = row.get("setting_http_version")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -1433,6 +1499,7 @@ impl UpsertModelInfo for HttpRequest {
.unwrap_or_default(),
setting_request_timeout: serde_json::from_str(&setting_request_timeout)
.unwrap_or_default(),
setting_http_version: serde_json::from_str(&setting_http_version).unwrap_or_default(),
})
}
}
@@ -208,6 +208,14 @@ impl<'a> ClientDb<'a> {
} else {
parent.store_cookies
},
http_version: if folder.setting_http_version.enabled {
ResolvedSetting::from_model(
folder.setting_http_version.value,
AnyModel::Folder(folder.clone()),
)
} else {
parent.http_version
},
})
}
}
@@ -153,6 +153,14 @@ impl<'a> ClientDb<'a> {
} else {
parent.store_cookies
},
http_version: if http_request.setting_http_version.enabled {
ResolvedSetting::from_model(
http_request.setting_http_version.value,
AnyModel::HttpRequest(http_request.clone()),
)
} else {
parent.http_version
},
})
}
@@ -174,7 +182,10 @@ impl<'a> ClientDb<'a> {
#[cfg(test)]
mod tests {
use crate::init_in_memory;
use crate::models::{HttpRequest, HttpRequestHeader};
use crate::models::{
Folder, HttpRequest, HttpRequestHeader, HttpVersion, InheritedHttpVersionSetting, Workspace,
};
use crate::util::UpdateSource;
#[test]
fn request_resolution_preserves_duplicate_request_headers() {
@@ -210,4 +221,77 @@ mod tests {
assert_eq!(cookies[1].value, "optional=1");
assert!(!cookies[1].enabled);
}
#[test]
fn http_version_resolves_through_the_inheritance_chain() {
let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
let db = query_manager.connect();
let workspace = db
.upsert_workspace(
&Workspace {
name: "Test".to_string(),
setting_http_version: HttpVersion::Http2,
..Default::default()
},
&UpdateSource::Background,
)
.expect("Failed to upsert workspace");
let folder = db
.upsert_folder(
&Folder { workspace_id: workspace.id.clone(), ..Default::default() },
&UpdateSource::Background,
)
.expect("Failed to upsert folder");
let request = db
.upsert_http_request(
&HttpRequest {
workspace_id: workspace.id.clone(),
folder_id: Some(folder.id.clone()),
..Default::default()
},
&UpdateSource::Background,
)
.expect("Failed to upsert request");
// No overrides, so the workspace base value applies
let resolved = db.resolve_settings_for_http_request(&request).expect("Failed to resolve");
assert_eq!(resolved.http_version.value, HttpVersion::Http2);
assert_eq!(resolved.http_version.source_model, "workspace");
// A folder override beats the workspace base
db.upsert_folder(
&Folder {
setting_http_version: InheritedHttpVersionSetting {
enabled: true,
value: HttpVersion::Http1,
},
..folder
},
&UpdateSource::Background,
)
.expect("Failed to update folder");
let resolved = db.resolve_settings_for_http_request(&request).expect("Failed to resolve");
assert_eq!(resolved.http_version.value, HttpVersion::Http1);
assert_eq!(resolved.http_version.source_model, "folder");
// A request override beats them both
let request = db
.upsert_http_request(
&HttpRequest {
setting_http_version: InheritedHttpVersionSetting {
enabled: true,
value: HttpVersion::Auto,
},
..request
},
&UpdateSource::Background,
)
.expect("Failed to update request");
let resolved = db.resolve_settings_for_http_request(&request).expect("Failed to resolve");
assert_eq!(resolved.http_version.value, HttpVersion::Auto);
assert_eq!(resolved.http_version.source_model, "http_request");
}
}
+6 -2
View File
@@ -96,8 +96,8 @@ impl<'a> ClientDb<'a> {
deleted
}
Err(e) => {
let _ = conn
.execute_batch("ROLLBACK TO delete_workspace; RELEASE delete_workspace");
let _ =
conn.execute_batch("ROLLBACK TO delete_workspace; RELEASE delete_workspace");
return Err(e);
}
};
@@ -177,6 +177,10 @@ impl<'a> ClientDb<'a> {
workspace.setting_store_cookies,
AnyModel::Workspace(workspace.clone()),
),
http_version: ResolvedSetting::from_model(
workspace.setting_http_version,
AnyModel::Workspace(workspace.clone()),
),
}
}
}