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
+452 -70
View File
@@ -1,127 +1,509 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
export type AnyModel =
| CookieJar
| Environment
| Folder
| GraphQlIntrospection
| GrpcConnection
| GrpcEvent
| GrpcRequest
| HttpRequest
| HttpResponse
| HttpResponseEvent
| KeyValue
| Plugin
| Settings
| SyncState
| WebsocketConnection
| WebsocketEvent
| WebsocketRequest
| Workspace
| WorkspaceMeta;
export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, };
export type ClientCertificate = {
host: string;
port: number | null;
crtFile: string | null;
keyFile: string | null;
pfxFile: string | null;
passphrase: string | null;
enabled?: boolean;
};
export type Cookie = { name: string, value: string, domain: CookieDomain, expires: CookieExpires, path: string, secure: boolean, httpOnly: boolean, sameSite: CookieSameSite | null, };
export type Cookie = {
name: string;
value: string;
domain: CookieDomain;
expires: CookieExpires;
path: string;
secure: boolean;
httpOnly: boolean;
sameSite: CookieSameSite | null;
};
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
export type CookieDomain = { HostOnly: string } | { Suffix: string } | "NotPresent" | "Empty";
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
export type CookieExpires = { AtUtc: string } | "SessionEnd";
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
export type CookieJar = {
model: "cookie_jar";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
cookies: Array<Cookie>;
name: string;
};
export type CookieSameSite = "Strict" | "Lax" | "None";
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
export type DnsOverride = {
hostname: string;
ipv4: Array<string>;
ipv6: Array<string>;
enabled?: boolean;
};
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type EncryptedKey = { encryptedKey: string };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
/**
* Variables defined in this environment scope.
* Child environments override parent variables by name.
*/
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
export type Environment = {
model: "environment";
id: string;
workspaceId: string;
createdAt: string;
updatedAt: string;
name: string;
public: boolean;
parentModel: string;
parentId: string | null;
/**
* Variables defined in this environment scope.
* Child environments override parent variables by name.
*/
variables: Array<EnvironmentVariable>;
color: string | null;
sortPriority: number;
};
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, settingSendCookies: InheritedBoolSetting, settingStoreCookies: InheritedBoolSetting, settingValidateCertificates: InheritedBoolSetting, settingFollowRedirects: InheritedBoolSetting, settingRequestTimeout: InheritedIntSetting, settingRequestMessageSize: InheritedIntSetting, };
export type Folder = {
model: "folder";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
folderId: string | null;
authentication: Record<string, any>;
authenticationType: string | null;
description: string;
headers: Array<HttpRequestHeader>;
name: string;
sortPriority: number;
settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type GraphQlIntrospection = { model: "graphql_introspection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, content: string | null, };
export type GraphQlIntrospection = {
model: "graphql_introspection";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
content: string | null;
};
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
export type GrpcConnection = {
model: "grpc_connection";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
elapsed: number;
error: string | null;
method: string;
service: string;
status: number;
state: GrpcConnectionState;
trailers: { [key in string]?: string };
url: string;
};
export type GrpcConnectionState = "initialized" | "connected" | "closed";
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
export type GrpcEvent = {
model: "grpc_event";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
connectionId: string;
content: string;
error: string | null;
eventType: GrpcEventType;
metadata: { [key in string]?: string };
status: number | null;
};
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
export type GrpcEventType =
| "info"
| "error"
| "client_message"
| "server_message"
| "connection_start"
| "connection_end";
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
/**
* Server URL (http for plaintext or https for secure)
*/
url: string, settingValidateCertificates: InheritedBoolSetting, settingRequestMessageSize: InheritedIntSetting, };
export type GrpcRequest = {
model: "grpc_request";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
folderId: string | null;
authenticationType: string | null;
authentication: Record<string, any>;
description: string;
message: string;
metadata: Array<HttpRequestHeader>;
method: string | null;
name: string;
service: string | null;
sortPriority: number;
/**
* Server URL (http for plaintext or https for secure)
*/
url: string;
settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
};
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, settingSendCookies: InheritedBoolSetting, settingStoreCookies: InheritedBoolSetting, settingValidateCertificates: InheritedBoolSetting, settingFollowRedirects: InheritedBoolSetting, settingRequestTimeout: InheritedIntSetting, };
export type HttpRequest = {
model: "http_request";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
folderId: string | null;
authentication: Record<string, any>;
authenticationType: string | null;
body: Record<string, any>;
bodyType: string | null;
description: string;
headers: Array<HttpRequestHeader>;
method: string;
name: string;
sortPriority: number;
url: string;
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>;
settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
export type HttpResponse = {
model: "http_response";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
contentLength: number | null;
contentLengthCompressed: number | null;
elapsed: number;
elapsedHeaders: number;
elapsedDns: number;
error: string | null;
headers: Array<HttpResponseHeader>;
remoteAddr: string | null;
requestContentLength: number | null;
requestHeaders: Array<HttpResponseHeader>;
status: number;
statusReason: string | null;
state: HttpResponseState;
url: string;
version: string | null;
};
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
export type HttpResponseEvent = {
model: "http_response_event";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
responseId: string;
event: HttpResponseEventData;
};
/**
* Serializable representation of HTTP response events for DB storage.
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
* The `From` impl is in yaak-http to avoid circular dependencies.
*/
export type HttpResponseEventData = { "type": "setting", name: string, value: string, source_model?: string, source_id?: string, source_name?: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseEventData =
| {
type: "setting";
name: string;
value: string;
source_model?: string;
source_id?: string;
source_name?: string;
}
| { type: "info"; message: string }
| {
type: "redirect";
url: string;
status: number;
behavior: string;
dropped_body: boolean;
dropped_headers: Array<string>;
}
| {
type: "send_url";
method: string;
scheme: string;
username: string;
password: string;
host: string;
port: number;
path: string;
query: string;
fragment: string;
}
| { type: "receive_url"; version: string; status: string }
| { type: "header_up"; name: string; value: string }
| { type: "header_down"; name: string; value: string }
| { type: "chunk_sent"; bytes: number }
| { type: "chunk_received"; bytes: number }
| {
type: "dns_resolved";
hostname: string;
addresses: Array<string>;
duration: bigint;
overridden: boolean;
};
export type HttpResponseHeader = { name: string, value: string, };
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 Yaak server, and what the server 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`
* Other entries are appended as query parameters
*/
name: string;
value: string;
id?: string;
};
export type HttpUrlParameter = { enabled?: boolean,
/**
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
* Other entries are appended as query parameters
*/
name: string, value: string, id?: string, };
export type HttpVersion = "auto" | "http1" | "http2";
export type InheritedBoolSetting = { enabled?: boolean, value: boolean, };
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
export type InheritedIntSetting = { enabled?: boolean, value: number, };
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type InheritedIntSetting = { enabled?: boolean; value: number };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
export type KeyValue = {
model: "key_value";
id: string;
createdAt: string;
updatedAt: string;
key: string;
namespace: string;
value: string;
};
export type Plugin = {
model: "plugin";
id: string;
createdAt: string;
updatedAt: string;
checkedAt: string | null;
directory: string;
enabled: boolean;
url: string | null;
source: PluginSource;
};
export type PluginSource = "bundled" | "filesystem" | "registry";
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
export type ProxySetting =
| {
type: "enabled";
http: string;
https: string;
auth: ProxySettingAuth | null;
bypass: string;
disabled: boolean;
}
| { type: "disabled" };
export type ProxySettingAuth = { user: string, password: string, };
export type ProxySettingAuth = { user: string; password: string };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array<ClientCertificate>, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, promptFeedback: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, hotkeys: { [key in string]?: Array<string> }, };
export type Settings = {
model: "settings";
id: string;
createdAt: string;
updatedAt: string;
appearance: string;
clientCertificates: Array<ClientCertificate>;
coloredMethods: boolean;
editorFont: string | null;
editorFontSize: number;
editorKeymap: EditorKeymap;
editorSoftWrap: boolean;
hideWindowControls: boolean;
useNativeTitlebar: boolean;
interfaceFont: string | null;
interfaceFontSize: number;
interfaceScale: number;
openWorkspaceNewWindow: boolean | null;
proxy: ProxySetting | null;
themeDark: string;
themeLight: string;
updateChannel: string;
hideLicenseBadge: boolean;
promptFeedback: boolean;
autoupdate: boolean;
autoDownloadUpdates: boolean;
checkNotifications: boolean;
hotkeys: { [key in string]?: Array<string> };
};
export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest;
export type SyncModel =
| ({ type: "workspace" } & Workspace)
| ({ type: "environment" } & Environment)
| ({ type: "folder" } & Folder)
| ({ type: "http_request" } & HttpRequest)
| ({ type: "grpc_request" } & GrpcRequest)
| ({ type: "websocket_request" } & WebsocketRequest);
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
export type SyncState = {
model: "sync_state";
id: string;
workspaceId: string;
createdAt: string;
updatedAt: string;
flushedAt: string;
modelId: string;
checksum: string;
relPath: string;
syncDir: string;
};
export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array<HttpResponseHeader>, state: WebsocketConnectionState, status: number, url: string, };
export type WebsocketConnection = {
model: "websocket_connection";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
elapsed: number;
error: string | null;
headers: Array<HttpResponseHeader>;
state: WebsocketConnectionState;
status: number;
url: string;
};
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array<number>, messageType: WebsocketEventType, };
export type WebsocketEvent = {
model: "websocket_event";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
connectionId: string;
isServer: boolean;
message: Array<number>;
messageType: WebsocketEventType;
};
export type WebsocketEventType = "binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text";
export type WebsocketEventType =
| "binary"
| "close"
| "error"
| "frame"
| "open"
| "ping"
| "pong"
| "text";
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, settingSendCookies: InheritedBoolSetting, settingStoreCookies: InheritedBoolSetting, settingValidateCertificates: InheritedBoolSetting, settingRequestMessageSize: InheritedIntSetting, };
export type WebsocketRequest = {
model: "websocket_request";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
folderId: string | null;
authentication: Record<string, any>;
authenticationType: string | null;
description: string;
headers: Array<HttpRequestHeader>;
message: string;
name: string;
sortPriority: number;
url: string;
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>;
settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
};
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingRequestMessageSize: number, settingDnsOverrides: Array<DnsOverride>, settingSendCookies: boolean, settingStoreCookies: boolean, };
export type Workspace = {
model: "workspace";
id: string;
createdAt: string;
updatedAt: string;
authentication: Record<string, any>;
authenticationType: string | null;
description: string;
headers: Array<HttpRequestHeader>;
name: string;
encryptionKeyChallenge: string | null;
settingValidateCertificates: boolean;
settingFollowRedirects: boolean;
settingRequestTimeout: number;
settingRequestMessageSize: number;
settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean;
settingStoreCookies: boolean;
settingHttpVersion: HttpVersion;
};
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
export type WorkspaceMeta = {
model: "workspace_meta";
id: string;
workspaceId: string;
createdAt: string;
updatedAt: string;
encryptionKey: EncryptedKey | null;
settingSyncDir: string | null;
};
+7
View File
@@ -47,6 +47,7 @@ export type Folder = {
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type GrpcRequest = {
@@ -99,6 +100,7 @@ export type HttpRequest = {
settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
@@ -114,8 +116,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 SyncModel =
@@ -169,4 +175,5 @@ export type Workspace = {
settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean;
settingStoreCookies: boolean;
settingHttpVersion: HttpVersion;
};
+24 -4
View File
@@ -3,7 +3,7 @@ use crate::error::Result;
use log::{debug, info, warn};
use reqwest::{Client, ClientBuilder, Proxy, redirect};
use std::sync::{Arc, Mutex};
use yaak_models::models::DnsOverride;
use yaak_models::models::{DnsOverride, HttpVersion};
use yaak_tls::{
ClientCertificateConfig, NativeClientIdentity, get_tls_config, load_native_client_identity,
};
@@ -39,6 +39,7 @@ impl ConfiguredClient {
/// supports TLS 1.0+ for legacy servers.
fn build_native_tls_connector(
client_cert: Option<ClientCertificateConfig>,
http_version: HttpVersion,
) -> Result<native_tls::TlsConnector> {
let mut builder = native_tls::TlsConnector::builder();
builder.danger_accept_invalid_certs(true);
@@ -46,7 +47,11 @@ fn build_native_tls_connector(
builder.min_protocol_version(Some(native_tls::Protocol::Tlsv10));
// reqwest cannot add ALPN to a connector it did not build, so without this
// the native path would silently negotiate HTTP/1.1 for every request.
builder.request_alpns(&["h2", "http/1.1"]);
match http_version {
HttpVersion::Auto => builder.request_alpns(&["h2", "http/1.1"]),
HttpVersion::Http1 => builder.request_alpns(&["http/1.1"]),
HttpVersion::Http2 => builder.request_alpns(&["h2"]),
};
if let Some(identity) = build_native_tls_identity(client_cert)? {
builder.identity(identity);
@@ -100,6 +105,7 @@ pub enum HttpConnectionProxySetting {
pub struct HttpConnectionOptions {
pub id: String,
pub validate_certificates: bool,
pub http_version: HttpVersion,
pub proxy: HttpConnectionProxySetting,
pub client_certificate: Option<ClientCertificateConfig>,
pub dns_overrides: Vec<DnsOverride>,
@@ -128,14 +134,28 @@ impl HttpConnectionOptions {
// This is needed so we can emit DNS timing events for each request
.pool_max_idle_per_host(0);
match self.http_version {
HttpVersion::Auto => {}
HttpVersion::Http1 => client = client.http1_only(),
HttpVersion::Http2 => client = client.http2_prior_knowledge(),
}
// Configure TLS
if self.validate_certificates {
// Use rustls with platform certificate verification (TLS 1.2+ only)
let config = get_tls_config(true, true, self.client_certificate.clone())?;
let mut config = get_tls_config(true, true, self.client_certificate.clone())?;
// A forced version must also constrain ALPN, or the server may
// negotiate a protocol the client then refuses to speak
match self.http_version {
HttpVersion::Auto => {}
HttpVersion::Http1 => config.alpn_protocols = vec![b"http/1.1".to_vec()],
HttpVersion::Http2 => config.alpn_protocols = vec![b"h2".to_vec()],
}
client = client.use_preconfigured_tls(config);
} else {
// Use native TLS for maximum compatibility (supports TLS 1.0+)
let connector = build_native_tls_connector(self.client_certificate.clone())?;
let connector =
build_native_tls_connector(self.client_certificate.clone(), self.http_version)?;
client = client.use_preconfigured_tls(connector);
}
+4 -1
View File
@@ -28,7 +28,10 @@ impl HttpConnectionManager {
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<CachedClient> {
let mut connections = self.connections.write().await;
let id = opt.id.clone();
// The key must include any per-request option that changes how the
// client is built, or a send after a settings change reuses a client
// built with the old value for up to the cache TTL.
let id = format!("{}::{}::{}", opt.id, opt.validate_certificates, opt.http_version);
// Clean old connections
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
+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()),
),
}
}
}
+16 -1
View File
@@ -109,6 +109,7 @@ export type Folder = {
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type GraphQlIntrospection = {
@@ -213,6 +214,7 @@ export type HttpRequest = {
settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
@@ -314,8 +316,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 = {
@@ -378,6 +384,7 @@ export type Settings = {
themeLight: string;
updateChannel: string;
hideLicenseBadge: boolean;
promptFeedback: boolean;
autoupdate: boolean;
autoDownloadUpdates: boolean;
checkNotifications: boolean;
@@ -428,7 +435,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 WebsocketRequest = {
model: "websocket_request";
@@ -473,6 +487,7 @@ export type Workspace = {
settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean;
settingStoreCookies: boolean;
settingHttpVersion: HttpVersion;
};
export type WorkspaceMeta = {
+7
View File
@@ -47,6 +47,7 @@ export type Folder = {
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type GrpcRequest = {
@@ -99,6 +100,7 @@ export type HttpRequest = {
settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting;
settingHttpVersion: InheritedHttpVersionSetting;
};
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
@@ -114,8 +116,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 SyncModel =
@@ -182,4 +188,5 @@ export type Workspace = {
settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean;
settingStoreCookies: boolean;
settingHttpVersion: HttpVersion;
};
+1
View File
@@ -190,6 +190,7 @@ impl SendRequestExecutor for ConnectionManagerSendRequestExecutor<'_> {
.get_client(&HttpConnectionOptions {
id: self.plugin_context_id.clone(),
validate_certificates: runtime_config.settings.validate_certificates.value,
http_version: runtime_config.settings.http_version.value,
proxy: runtime_config.proxy.clone(),
client_certificate,
dns_overrides: runtime_config.dns_overrides.clone(),