Implement transport profile routing

This commit is contained in:
fawney19
2026-05-05 22:21:23 +08:00
parent aacab1a90c
commit f959f02d40
86 changed files with 860 additions and 318 deletions
@@ -46,8 +46,7 @@ use crate::ai_serving::transport::vertex::{
use crate::ai_serving::transport::{
apply_local_body_rules, apply_local_header_rules, build_passthrough_headers,
ensure_upstream_auth_header, resolve_transport_execution_timeouts,
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
LocalResolvedOAuthRequestAuth,
resolve_transport_proxy_snapshot_with_tunnel_affinity, LocalResolvedOAuthRequestAuth,
};
use crate::ai_serving::{
collect_control_headers, ConversionMode, ExecutionStrategy, GatewayControlDecision,
@@ -18,7 +18,7 @@ use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
resolve_transport_execution_timeouts, resolve_transport_profile,
};
use crate::{
append_execution_contract_fields_to_value, append_local_failover_policy_to_value,
@@ -66,7 +66,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
let proxy = state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport)
.await;
let tls_profile = resolve_transport_tls_profile(&resolved.transport);
let transport_profile = resolve_transport_profile(&resolved.transport);
let mut extra_fields = serde_json::Map::new();
if let Some(proxy_value) =
build_request_trace_proxy_value(Some(&resolved.transport), proxy.as_ref())
@@ -173,7 +173,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy,
tls_profile,
transport_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
upstream_is_stream,
report_kind: Some(report_kind.to_string()),
@@ -9,7 +9,7 @@ use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
resolve_transport_execution_timeouts, resolve_transport_profile,
};
use crate::ai_serving::{ai_local_execution_contract_for_formats, PlannerAppState};
use crate::{AiExecutionDecision, AppState};
@@ -62,7 +62,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
.app()
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await;
let tls_profile = resolve_transport_tls_profile(&transport);
let transport_profile = resolve_transport_profile(&transport);
let mut extra_fields = serde_json::Map::new();
if let Some(proxy_value) = build_request_trace_proxy_value(Some(&transport), proxy.as_ref()) {
extra_fields.insert("proxy".to_string(), proxy_value);
@@ -147,7 +147,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
proxy,
tls_profile,
transport_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
upstream_is_stream: spec_metadata.require_streaming,
report_kind: spec_metadata.report_kind.map(ToOwned::to_owned),
@@ -7,7 +7,7 @@ use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
resolve_transport_execution_timeouts, resolve_transport_profile,
};
use crate::ai_serving::{ai_local_execution_contract_for_formats, PlannerAppState};
use crate::{AiExecutionDecision, AppState};
@@ -53,7 +53,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
.app()
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await;
let tls_profile = resolve_transport_tls_profile(&transport);
let transport_profile = resolve_transport_profile(&transport);
let mut extra_fields = serde_json::Map::new();
if let Some(proxy_value) = build_request_trace_proxy_value(Some(&transport), proxy.as_ref()) {
extra_fields.insert("proxy".to_string(), proxy_value);
@@ -126,7 +126,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy,
tls_profile,
transport_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
upstream_is_stream,
report_kind: spec_metadata.report_kind.map(ToOwned::to_owned),
@@ -7,7 +7,7 @@ use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
resolve_transport_execution_timeouts, resolve_transport_profile,
};
use crate::ai_serving::{ai_local_execution_contract_for_formats, PlannerAppState};
use crate::{AiExecutionDecision, AppState};
@@ -45,7 +45,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
.app()
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await;
let tls_profile = resolve_transport_tls_profile(&transport);
let transport_profile = resolve_transport_profile(&transport);
let mut extra_fields = serde_json::Map::new();
if let Some(proxy_value) = build_request_trace_proxy_value(Some(&transport), proxy.as_ref()) {
extra_fields.insert("proxy".to_string(), proxy_value);
@@ -127,7 +127,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
proxy,
tls_profile,
transport_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
upstream_is_stream: false,
report_kind: spec_metadata.report_kind.map(ToOwned::to_owned),
@@ -16,7 +16,7 @@ use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
resolve_transport_execution_timeouts, resolve_transport_profile,
};
use crate::ai_serving::{
ai_local_execution_contract_for_formats, api_format_alias_matches,
@@ -139,7 +139,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
),
&resolved.transport,
);
let tls_profile = resolve_transport_tls_profile(&resolved.transport);
let transport_profile = resolve_transport_profile(&resolved.transport);
let timeouts = resolve_transport_execution_timeouts(&resolved.transport);
let super::request::LocalStandardCandidatePayloadParts {
auth_header,
@@ -181,7 +181,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy,
tls_profile,
transport_profile,
timeouts,
upstream_is_stream,
report_kind: spec_metadata.report_kind.map(ToOwned::to_owned),
@@ -7,7 +7,7 @@ use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
resolve_transport_execution_timeouts, resolve_transport_profile,
};
use crate::{
append_execution_contract_fields_to_value, append_local_failover_policy_to_value,
@@ -62,7 +62,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
let proxy = state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport)
.await;
let tls_profile = resolve_transport_tls_profile(&resolved.transport);
let transport_profile = resolve_transport_profile(&resolved.transport);
let timeouts = resolve_transport_execution_timeouts(&resolved.transport);
let mut extra_fields = serde_json::Map::new();
if let Some(proxy_value) =
@@ -171,7 +171,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy,
tls_profile,
transport_profile,
timeouts,
upstream_is_stream,
report_kind: Some(report_kind),
@@ -297,7 +297,7 @@ mod tests {
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_responses_stream_success".to_string()),
@@ -393,7 +393,7 @@ mod tests {
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_chat_stream_success".to_string()),
@@ -468,7 +468,7 @@ mod tests {
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_chat_stream_success".to_string()),
@@ -284,7 +284,7 @@ mod tests {
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: true,
report_kind: Some("openai_responses_sync_success".to_string()),
@@ -379,7 +379,7 @@ mod tests {
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: false,
report_kind: Some("openai_chat_sync_success".to_string()),
@@ -450,7 +450,7 @@ mod tests {
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: false,
report_kind: Some("openai_chat_sync_success".to_string()),
@@ -11,7 +11,7 @@ use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
resolve_transport_execution_timeouts, resolve_transport_profile,
};
use crate::{
append_execution_contract_fields_to_value, append_local_failover_policy_to_value,
@@ -63,7 +63,7 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand
let proxy = state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport)
.await;
let tls_profile = resolve_transport_tls_profile(&resolved.transport);
let transport_profile = resolve_transport_profile(&resolved.transport);
let timeouts = resolve_transport_execution_timeouts(&resolved.transport);
let mut extra_fields = serde_json::Map::new();
if let Some(proxy_value) =
@@ -194,7 +194,7 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy,
tls_profile,
transport_profile,
timeouts,
upstream_is_stream,
report_kind: spec_metadata.report_kind.map(ToOwned::to_owned),
@@ -70,11 +70,12 @@ pub(crate) use aether_provider_transport::{
request_conversion_enabled_for_transport, request_conversion_transport_supported,
request_conversion_transport_unsupported_reason, request_pair_allowed_for_transport,
resolve_gemini_files_auth, resolve_openai_image_auth, resolve_same_format_provider_direct_auth,
resolve_transport_execution_timeouts, resolve_transport_proxy_snapshot,
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
resolve_video_create_auth, same_format_provider_transport_supported,
same_format_provider_transport_unsupported_reason, should_skip_upstream_passthrough_header,
should_try_same_format_provider_oauth_auth, supports_local_gemini_transport_with_network,
resolve_transport_execution_timeouts, resolve_transport_profile,
resolve_transport_proxy_snapshot, resolve_transport_proxy_snapshot_with_tunnel_affinity,
resolve_transport_tls_profile, resolve_video_create_auth,
same_format_provider_transport_supported, same_format_provider_transport_unsupported_reason,
should_skip_upstream_passthrough_header, should_try_same_format_provider_oauth_auth,
supports_local_gemini_transport_with_network,
supports_local_generic_oauth_request_auth_resolution,
supports_local_oauth_request_auth_resolution, transport_proxy_is_locally_supported,
video_create_transport_unsupported_reason, CandidateTransportPolicyFacts,
@@ -364,7 +364,7 @@ mod tests {
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
}
}
@@ -307,7 +307,7 @@ async fn execute_mcp_request(
provider_api_format: plan.provider_api_format.clone(),
model_name: plan.model_name.clone(),
proxy: plan.proxy.clone(),
tls_profile: plan.tls_profile.clone(),
transport_profile: plan.transport_profile.clone(),
timeouts: plan.timeouts.clone(),
};
let result = DirectSyncExecutionRuntime::new()
@@ -538,7 +538,7 @@ async fn discover_kiro_profile_arn_in_region(
provider_api_format: "kiro:profiles".to_string(),
model_name: Some("kiro-list-available-profiles".to_string()),
proxy: plan.proxy.clone(),
tls_profile: plan.tls_profile.clone(),
transport_profile: plan.transport_profile.clone(),
timeouts: plan.timeouts.clone(),
};
let result = DirectSyncExecutionRuntime::new()
@@ -1208,7 +1208,7 @@ mod tests {
provider_api_format: "claude:messages".to_string(),
model_name: Some("claude-sonnet-4.6".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
}
}
@@ -352,6 +352,7 @@ impl IntoResponse for ExecutionRuntimeAppError {
| ExecutionRuntimeTransportError::InvalidHeaderName(_)
| ExecutionRuntimeTransportError::InvalidHeaderValue(_)
| ExecutionRuntimeTransportError::InvalidProxy(_)
| ExecutionRuntimeTransportError::UnsupportedTransportProfile(_)
| ExecutionRuntimeTransportError::BodyEncode(_),
) => StatusCode::BAD_REQUEST,
ExecutionRuntimeServerError::Transport(
@@ -422,7 +423,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-4.1".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(30_000),
@@ -2504,7 +2504,7 @@ mod tests {
provider_api_format: "openai:responses".into(),
model_name: Some("gpt-5.4".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2625,7 +2625,7 @@ mod tests {
provider_api_format: "openai:responses".into(),
model_name: Some("gpt-5.4".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2733,7 +2733,7 @@ mod tests {
provider_api_format: "openai:image".into(),
model_name: Some("gpt-image-1".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2822,7 +2822,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-5".into()),
proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2951,7 +2951,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-5".into()),
proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -777,7 +777,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-5".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -868,7 +868,7 @@ mod tests {
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-5".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -986,7 +986,7 @@ mod tests {
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -1115,7 +1115,7 @@ mod tests {
provider_api_format: "openai:image".to_string(),
model_name: Some("gpt-image-1".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -1224,7 +1224,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-5".into()),
proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -1368,7 +1368,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-5".into()),
proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -64,7 +64,7 @@ fn missing_exact_provider_request_payload(decision_kind: &str) -> AiExecutionDec
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: false,
report_kind: Some("openai_chat_sync_success".to_string()),
@@ -806,7 +806,7 @@ fn bypasses_execution_runtime_for_codex_plan_variant() {
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
};
@@ -5,8 +5,9 @@ use std::io::Write;
use std::time::{Duration, Instant};
use aether_contracts::{
ExecutionPlan, ExecutionResult, ExecutionTelemetry, ProxySnapshot, ResponseBody,
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER,
ExecutionPlan, ExecutionResult, ExecutionTelemetry, ProxySnapshot, ResolvedTransportProfile,
ResponseBody, EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER,
TRANSPORT_BACKEND_REQWEST_RUSTLS, TRANSPORT_HTTP_MODE_HTTP1_ONLY,
};
use aether_data::repository::proxy_nodes::ProxyNodeTrafficMutation;
use aether_http::{apply_http_client_config, HttpClientConfig};
@@ -102,6 +103,8 @@ pub(crate) enum ExecutionRuntimeTransportError {
InvalidHeaderValue(String),
#[error("invalid proxy configuration: {0}")]
InvalidProxy(reqwest::Error),
#[error("unsupported transport profile backend: {0}")]
UnsupportedTransportProfile(String),
#[error("failed to encode request body: {0}")]
BodyEncode(serde_json::Error),
#[error("failed to build HTTP client: {0}")]
@@ -116,6 +119,9 @@ pub(crate) enum ExecutionRuntimeTransportError {
#[derive(Debug, Serialize)]
struct RelayRequestMeta {
provider_id: String,
endpoint_id: String,
key_id: String,
method: String,
url: String,
headers: BTreeMap<String, String>,
@@ -124,6 +130,8 @@ struct RelayRequestMeta {
follow_redirects: Option<bool>,
#[serde(default, skip_serializing_if = "is_false")]
http1_only: bool,
#[serde(skip_serializing_if = "Option::is_none")]
transport_profile: Option<ResolvedTransportProfile>,
}
#[derive(Debug, Clone, Default)]
@@ -531,12 +539,16 @@ fn build_direct_tunnel_request_meta(
transport_controls: ExecutionTransportControls,
) -> tunnel_protocol::RequestMeta {
tunnel_protocol::RequestMeta {
provider_id: Some(plan.provider_id.clone()),
endpoint_id: Some(plan.endpoint_id.clone()),
key_id: Some(plan.key_id.clone()),
method: plan.method.clone(),
url: plan.url.clone(),
headers: header_map_to_string_map(headers).into_iter().collect(),
timeout: resolve_relay_timeout_seconds(plan),
follow_redirects: transport_controls.follow_redirects,
http1_only: transport_controls.http1_only,
transport_profile: plan.transport_profile.clone(),
}
}
@@ -577,7 +589,7 @@ async fn send_request(
let client = build_client(
plan.timeouts.as_ref(),
plan.proxy.as_ref(),
plan.tls_profile.as_deref(),
plan.transport_profile.as_ref(),
transport_controls,
)?;
let mut request = client.request(method, &plan.url);
@@ -604,12 +616,16 @@ async fn send_via_tunnel_relay(
let timeout_secs = resolve_relay_timeout_seconds(plan);
let envelope = build_relay_envelope(
RelayRequestMeta {
provider_id: plan.provider_id.clone(),
endpoint_id: plan.endpoint_id.clone(),
key_id: plan.key_id.clone(),
method: method.as_str().to_string(),
url: plan.url.clone(),
headers: header_map_to_string_map(&headers),
timeout: timeout_secs,
follow_redirects: transport_controls.follow_redirects,
http1_only: transport_controls.http1_only,
transport_profile: plan.transport_profile.clone(),
},
&body_bytes,
)?;
@@ -856,14 +872,15 @@ fn resolve_local_tunnel_node_id(state: &AppState, proxy: Option<&ProxySnapshot>)
fn build_client(
timeouts: Option<&aether_contracts::ExecutionTimeouts>,
proxy: Option<&ProxySnapshot>,
tls_profile: Option<&str>,
transport_profile: Option<&ResolvedTransportProfile>,
transport_controls: ExecutionTransportControls,
) -> Result<reqwest::Client, ExecutionRuntimeTransportError> {
validate_reqwest_transport_profile(transport_profile)?;
let mut builder = reqwest::Client::builder();
if transport_controls.follow_redirects != Some(true) {
builder = builder.redirect(Policy::none());
}
if transport_controls.http1_only {
if transport_controls.http1_only || transport_profile_http1_only(transport_profile) {
builder = builder.http1_only();
}
let mut builder = apply_http_client_config(
@@ -873,7 +890,10 @@ fn build_client(
..HttpClientConfig::default()
},
);
builder = apply_tls_profile(builder, tls_profile);
builder = apply_tls_profile(
builder,
transport_profile.map(|profile| profile.profile_id.as_str()),
);
if let Some(proxy_url) = resolve_proxy_url(proxy)? {
let proxy = reqwest::Proxy::all(&proxy_url)
.map_err(ExecutionRuntimeTransportError::InvalidProxy)?;
@@ -884,11 +904,40 @@ fn build_client(
.map_err(ExecutionRuntimeTransportError::ClientBuild)
}
fn validate_reqwest_transport_profile(
transport_profile: Option<&ResolvedTransportProfile>,
) -> Result<(), ExecutionRuntimeTransportError> {
let Some(profile) = transport_profile else {
return Ok(());
};
if profile
.backend
.trim()
.eq_ignore_ascii_case(TRANSPORT_BACKEND_REQWEST_RUSTLS)
{
return Ok(());
}
Err(ExecutionRuntimeTransportError::UnsupportedTransportProfile(
profile.backend.clone(),
))
}
fn transport_profile_http1_only(transport_profile: Option<&ResolvedTransportProfile>) -> bool {
transport_profile
.map(|profile| {
profile
.http_mode
.trim()
.eq_ignore_ascii_case(TRANSPORT_HTTP_MODE_HTTP1_ONLY)
})
.unwrap_or(false)
}
fn apply_tls_profile(
builder: reqwest::ClientBuilder,
tls_profile: Option<&str>,
profile_id: Option<&str>,
) -> reqwest::ClientBuilder {
let profile = normalize_tls_profile(tls_profile);
let profile = normalize_tls_profile(profile_id);
if profile.is_none() {
return builder;
}
@@ -1136,7 +1185,7 @@ mod tests {
use std::sync::Arc;
use aether_contracts::{
ExecutionPlan, ExecutionTimeouts, ProxySnapshot, RequestBody,
ExecutionPlan, ExecutionTimeouts, ProxySnapshot, RequestBody, ResolvedTransportProfile,
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER,
};
use aether_data::repository::proxy_nodes::{
@@ -1154,7 +1203,8 @@ mod tests {
use super::{
build_client, execute_sync_plan, record_manual_proxy_request_failure,
record_manual_proxy_request_outcome, record_manual_proxy_request_success,
record_manual_proxy_stream_error, DirectSyncExecutionRuntime, ExecutionTransportControls,
record_manual_proxy_stream_error, DirectSyncExecutionRuntime,
ExecutionRuntimeTransportError, ExecutionTransportControls,
};
use crate::constants::{
EXECUTION_RUNTIME_LOOP_GUARD_HEADER, EXECUTION_RUNTIME_LOOP_GUARD_VIA_TOKEN,
@@ -1346,7 +1396,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-4.1".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -1394,7 +1444,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: None,
proxy: Some(manual_proxy_snapshot("manual-node-1")),
tls_profile: None,
transport_profile: None,
timeouts: None,
};
@@ -1438,7 +1488,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: None,
proxy: Some(manual_proxy_snapshot("manual-node-1")),
tls_profile: None,
transport_profile: None,
timeouts: None,
};
@@ -1482,7 +1532,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: None,
proxy: Some(manual_proxy_snapshot("manual-node-1")),
tls_profile: None,
transport_profile: None,
timeouts: None,
};
@@ -1526,7 +1576,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: None,
proxy: Some(manual_proxy_snapshot("manual-node-1")),
tls_profile: None,
transport_profile: None,
timeouts: None,
};
@@ -1570,7 +1620,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: None,
proxy: Some(manual_proxy_snapshot("manual-node-1")),
tls_profile: None,
transport_profile: None,
timeouts: None,
};
@@ -1616,7 +1666,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: None,
proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())),
tls_profile: None,
transport_profile: None,
timeouts: None,
};
@@ -1696,7 +1746,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-4.1".into()),
proxy: Some(tunnel_proxy_snapshot(format!("http://{addr}"))),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -1748,7 +1798,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-4.1".into()),
proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -1897,7 +1947,7 @@ mod tests {
provider_api_format: "provider_ops:verify".into(),
model_name: Some("verify-auth".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -1976,7 +2026,7 @@ mod tests {
provider_api_format: "provider_oauth:exchange".into(),
model_name: Some("oauth-exchange".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2006,8 +2056,12 @@ mod tests {
post(|Path(node_id): Path<String>, body: Bytes| async move {
let (meta, request_body) = decode_relay_envelope(&body);
assert_eq!(node_id, "node-1");
assert_eq!(meta["provider_id"], "prov-1");
assert_eq!(meta["endpoint_id"], "ep-1");
assert_eq!(meta["key_id"], "key-1");
assert_eq!(meta["http1_only"], true);
assert_eq!(meta["follow_redirects"], json!(false));
assert_eq!(meta["transport_profile"]["profile_id"], "relay-profile");
let request_json: serde_json::Value =
serde_json::from_slice(&request_body).expect("request body should be json");
assert_eq!(request_json["model"], "gpt-4.1");
@@ -2047,7 +2101,9 @@ mod tests {
provider_api_format: "provider_ops:verify".into(),
model_name: Some("verify-auth".into()),
proxy: Some(tunnel_proxy_snapshot(format!("http://{addr}"))),
tls_profile: None,
transport_profile: Some(ResolvedTransportProfile::from_legacy_tls_profile(
"relay-profile",
)),
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2107,7 +2163,7 @@ mod tests {
provider_api_format: "claude:messages".into(),
model_name: Some("claude-3.7-sonnet".into()),
proxy: None,
tls_profile: Some("claude_code_nodejs".into()),
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2126,6 +2182,33 @@ mod tests {
);
}
#[test]
fn direct_sync_execution_runtime_rejects_unsupported_transport_backend() {
let profile = ResolvedTransportProfile {
profile_id: "chrome-120".into(),
backend: "utls".into(),
http_mode: "auto".into(),
pool_scope: "key".into(),
extra: None,
};
let error = match build_client(
None,
None,
Some(&profile),
ExecutionTransportControls::default(),
) {
Ok(_) => panic!("unsupported backend should fail"),
Err(error) => error,
};
assert!(matches!(
error,
ExecutionRuntimeTransportError::UnsupportedTransportProfile(backend)
if backend == "utls"
));
}
#[tokio::test]
async fn direct_sync_execution_runtime_compresses_json_body_when_requested() {
let listener = crate::test_support::bind_loopback_listener()
@@ -2182,7 +2265,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-4.1".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -2243,7 +2326,7 @@ mod tests {
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-4.1".into()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
@@ -582,7 +582,7 @@ mod tests {
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-test".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts,
}
}
@@ -210,7 +210,7 @@ async fn admin_gemini_files_upload_single_key(
proxy: state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await,
tls_profile: state.resolve_transport_tls_profile(&transport),
transport_profile: state.resolve_transport_profile(&transport),
timeouts: state.resolve_transport_execution_timeouts(&transport),
};
@@ -72,7 +72,7 @@ async fn execute_antigravity_quota_plan(
provider_api_format: "antigravity:fetch_available_models".to_string(),
model_name: Some("fetchAvailableModels".to_string()),
proxy,
tls_profile: state.resolve_transport_tls_profile(transport),
transport_profile: state.resolve_transport_profile(transport),
timeouts,
};
@@ -97,7 +97,7 @@ pub(super) async fn execute_codex_quota_plan(
provider_api_format: "openai:responses".to_string(),
model_name: Some("codex-wham-usage".to_string()),
proxy,
tls_profile: state.resolve_transport_tls_profile(transport),
transport_profile: state.resolve_transport_profile(transport),
timeouts,
};
execute_provider_quota_plan(state, transport, plan, "codex").await
@@ -103,7 +103,7 @@ pub(super) async fn execute_kiro_quota_plan(
provider_api_format: "kiro:usage".to_string(),
model_name: Some("kiro-usage-limits".to_string()),
proxy,
tls_profile: state.resolve_transport_tls_profile(transport),
transport_profile: state.resolve_transport_profile(transport),
timeouts,
};
@@ -194,7 +194,6 @@ pub(super) async fn execute_provider_quota_plan(
key_id = %transport.key.id,
endpoint_id = %transport.endpoint.id,
url = %plan.url,
tls_profile = ?plan.tls_profile.as_deref(),
proxy_source = ?proxy_source,
proxy_node_id = ?proxy_node_id,
proxy_url_present,
@@ -179,7 +179,7 @@ async fn admin_provider_ops_execute_request(
provider_api_format: "provider_ops:verify".to_string(),
model_name: Some("verify-auth".to_string()),
proxy: proxy_snapshot.cloned(),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(ADMIN_PROVIDER_OPS_VERIFY_TIMEOUT_MS),
read_ms: Some(ADMIN_PROVIDER_OPS_VERIFY_TIMEOUT_MS),
@@ -849,7 +849,7 @@ async fn provider_query_execute_kiro_test_candidate(
proxy: state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await,
tls_profile: state.resolve_transport_tls_profile(&transport),
transport_profile: state.resolve_transport_profile(&transport),
timeouts: state.resolve_transport_execution_timeouts(&transport),
};
@@ -1214,7 +1214,7 @@ async fn provider_query_execute_standard_test_candidate(
proxy: state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await,
tls_profile: state.resolve_transport_tls_profile(&transport),
transport_profile: state.resolve_transport_profile(&transport),
timeouts: state.resolve_transport_execution_timeouts(&transport),
};
@@ -44,6 +44,8 @@ pub(crate) struct AdminProviderKeyCreateRequest {
pub(crate) model_include_patterns: Option<Vec<String>>,
#[serde(default)]
pub(crate) model_exclude_patterns: Option<Vec<String>>,
#[serde(default)]
pub(crate) fingerprint: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
@@ -170,7 +170,7 @@ pub(crate) async fn build_admin_create_provider_key_record(
normalize_string_list(payload.allowed_models).map(|value| json!(value)),
None,
None,
None,
normalize_json_object(payload.fingerprint, "fingerprint")?,
)
.map_err(|err| err.to_string())?;
key.note = payload
@@ -221,6 +221,13 @@ impl<'a> AdminAppState<'a> {
crate::provider_transport::resolve_transport_tls_profile(transport)
}
pub(crate) fn resolve_transport_profile(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<aether_contracts::ResolvedTransportProfile> {
crate::provider_transport::resolve_transport_profile(transport)
}
pub(crate) fn resolve_transport_execution_timeouts(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
@@ -1261,12 +1261,16 @@ fn build_tunnel_probe_relay_envelope(
timeout_secs: u64,
) -> Result<Vec<u8>, String> {
let meta = crate::tunnel::tunnel_protocol::RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: probe_url.trim().to_string(),
headers: std::collections::HashMap::new(),
timeout: timeout_secs,
follow_redirects: Some(false),
http1_only: false,
transport_profile: None,
};
let meta_bytes = serde_json::to_vec(&meta)
.map_err(|error| format!("encode tunnel probe metadata failed: {error}"))?;
@@ -163,7 +163,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
if transport.provider.proxy.is_some()
|| transport.endpoint.proxy.is_some()
|| transport.key.proxy.is_some()
|| crate::provider_transport::resolve_transport_tls_profile(&transport).is_some()
|| crate::provider_transport::resolve_transport_profile(&transport).is_some()
{
return None;
}
@@ -69,7 +69,7 @@ impl<'a> OAuthHttpExecutor for GatewayOAuthHttpExecutor<'a> {
provider_api_format: "oauth:exchange".to_string(),
model_name: Some("oauth-exchange".to_string()),
proxy: request.network.proxy,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(timeouts.connect_ms),
read_ms: Some(timeouts.read_ms),
@@ -770,7 +770,7 @@ mod tests {
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
}
}
@@ -794,7 +794,7 @@ mod tests {
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
}
}
@@ -753,7 +753,7 @@ mod tests {
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
tls_profile: None,
transport_profile: None,
timeouts: None,
}
}
+1 -1
View File
@@ -1313,7 +1313,7 @@ impl AppState {
provider_api_format: "provider_oauth:local_refresh".to_string(),
model_name: Some(provider_type.to_string()),
proxy: proxy_snapshot,
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(LOCAL_OAUTH_HTTP_TIMEOUT_MS),
read_ms: Some(LOCAL_OAUTH_HTTP_TIMEOUT_MS),
@@ -568,7 +568,7 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz
profile_arn: String,
debug_tag: String,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -900,8 +900,8 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -1089,7 +1089,7 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz
"proxy-node-kiro-cli-finalize-local"
);
assert_eq!(
seen_remote_execution_runtime_request.tls_profile,
seen_remote_execution_runtime_request.transport_profile_id,
"chrome_136"
);
@@ -943,7 +943,7 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision
metadata_source: String,
temperature_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -1258,8 +1258,8 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -1391,7 +1391,10 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision
seen_execution_runtime_request.proxy_node_id,
"proxy-node-openai-custom-stream"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-openai-chat-custom-stream-123")
@@ -43,7 +43,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
instructions: String,
store_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -398,8 +398,8 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -534,7 +534,10 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
seen_execution_runtime_request.proxy_node_id,
"proxy-node-openai-compact-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-openai-compact-local-123")
@@ -37,7 +37,7 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
current_content: String,
debug_tag: String,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn crc32(data: &[u8]) -> u32 {
@@ -404,8 +404,8 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -549,7 +549,10 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
seen_execution_runtime_request.proxy_node_id,
"proxy-node-kiro-cli-local-stream"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-kiro-cli-local-stream-123")
@@ -588,7 +591,7 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait
metadata_mode: String,
metadata_source: String,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -866,8 +869,8 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -992,7 +995,10 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait
seen_execution_runtime_request.proxy_node_id,
"proxy-node-claude-cli-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-claude-cli-local-stream-123")
@@ -1037,7 +1043,7 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo
metadata_source: String,
assistant_content: serde_json::Value,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -1365,8 +1371,8 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -1517,7 +1523,7 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo
"proxy-node-claude-code-cli-local"
);
assert_eq!(
seen_execution_runtime_request.tls_profile,
seen_execution_runtime_request.transport_profile_id,
"claude_code_nodejs"
);
@@ -1557,7 +1563,7 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_
metadata_mode: String,
metadata_source: String,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -1838,8 +1844,8 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -1950,7 +1956,10 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_
seen_execution_runtime_request.proxy_node_id,
"proxy-node-claude-chat-stream"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-claude-chat-local-stream-123")
@@ -25,7 +25,7 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_
metadata_source: String,
tool_config_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -331,8 +331,8 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -441,7 +441,10 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_
seen_execution_runtime_request.proxy_node_id,
"proxy-node-gemini-chat-stream"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-gemini-chat-local-stream-1")
@@ -25,7 +25,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s
metadata_source: String,
tool_config_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -308,8 +308,8 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -418,7 +418,10 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s
seen_execution_runtime_request.proxy_node_id,
"proxy-node-gemini-cli-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-gemini-cli-local-stream-123")
@@ -457,7 +460,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
metadata_source: String,
tool_config_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
#[derive(Debug, Clone)]
@@ -782,8 +785,8 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -927,7 +930,10 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
seen_execution_runtime_request.proxy_node_id,
"proxy-node-gemini-cli-oauth-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-gemini-cli-oauth-local-stream-123")
@@ -2605,7 +2605,7 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g
metadata_source: String,
temperature_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -2890,8 +2890,9 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -3018,7 +3019,10 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g
seen_execution_runtime_request.proxy_node_id,
"proxy-node-openai-custom-path"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(
@@ -61,7 +61,7 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca
metadata_source: String,
assistant_content: serde_json::Value,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -393,8 +393,9 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -559,7 +560,7 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca
"proxy-node-claude-code-cli-local"
);
assert_eq!(
seen_execution_runtime_request.tls_profile,
seen_execution_runtime_request.transport_profile_id,
"claude_code_nodejs"
);
@@ -57,7 +57,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
profile_arn: String,
debug_tag: String,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn crc32(data: &[u8]) -> u32 {
@@ -430,8 +430,8 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile").and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -579,7 +579,10 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
seen_execution_runtime_request.proxy_node_id,
"proxy-node-kiro-cli-local-sync"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-kiro-cli-local-sync-123")
@@ -56,7 +56,7 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy
metadata_mode: String,
metadata_source: String,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -335,8 +335,9 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -459,7 +460,10 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy
seen_execution_runtime_request.proxy_node_id,
"proxy-node-claude-chat-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-claude-chat-local-123")
@@ -56,7 +56,7 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn
metadata_mode: String,
metadata_source: String,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -335,8 +335,9 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -467,7 +468,10 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn
seen_execution_runtime_request.proxy_node_id,
"proxy-node-claude-cli-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-claude-cli-local-sync-123")
@@ -43,7 +43,7 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc
metadata_origin: String,
store_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -364,8 +364,9 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -499,7 +500,10 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc
seen_execution_runtime_request.proxy_node_id,
"proxy-node-openai-cli-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-openai-cli-local-123")
@@ -55,7 +55,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
metadata_source: String,
tool_config_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -333,8 +333,9 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -452,7 +453,10 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
seen_execution_runtime_request.proxy_node_id,
"proxy-node-gemini-cli-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-gemini-cli-local-sync-123")
@@ -775,7 +779,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
metadata_source: String,
tool_config_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
#[derive(Debug, Clone)]
@@ -1095,8 +1099,9 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -1240,7 +1245,10 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
seen_execution_runtime_request.proxy_node_id,
"proxy-node-gemini-cli-oauth-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-gemini-cli-oauth-local-sync-123")
@@ -55,7 +55,7 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
metadata_source: String,
tool_config_present: bool,
proxy_node_id: String,
tls_profile: String,
transport_profile_id: String,
}
fn hash_api_key(value: &str) -> String {
@@ -333,8 +333,9 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tls_profile: payload
.get("tls_profile")
transport_profile_id: payload
.get("transport_profile")
.and_then(|value| value.get("profile_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -442,7 +443,10 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
seen_execution_runtime_request.proxy_node_id,
"proxy-node-gemini-chat-local"
);
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
assert_eq!(
seen_execution_runtime_request.transport_profile_id,
"chrome_136"
);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-gemini-chat-local-123")
@@ -1224,12 +1224,16 @@ mod tests {
fn build_meta() -> protocol::RequestMeta {
protocol::RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: "https://example.com".to_string(),
headers: HashMap::new(),
timeout: 30,
follow_redirects: None,
http1_only: false,
transport_profile: None,
}
}
@@ -632,12 +632,16 @@ mod tests {
)));
let meta = protocol::RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: "https://example.com/health".to_string(),
headers: HashMap::new(),
timeout: 30,
follow_redirects: None,
http1_only: false,
transport_profile: None,
};
let request = Request::builder()
.body(Body::from(encode_relay_envelope(&meta, &[])))
@@ -742,12 +746,16 @@ mod tests {
)));
let meta = protocol::RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: "https://example.com/headers".to_string(),
headers: HashMap::new(),
timeout: 30,
follow_redirects: None,
http1_only: false,
transport_profile: None,
};
let request = Request::builder()
.body(Body::from(encode_relay_envelope(&meta, &[])))
+4
View File
@@ -481,12 +481,16 @@ impl EmbeddedTunnelState {
) -> Result<TunnelProbeResponse, String> {
let timeout_secs = timeout_secs.clamp(5, 60);
let meta = tunnel_protocol::RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: url.trim().to_string(),
headers: HashMap::new(),
timeout: timeout_secs,
follow_redirects: Some(false),
http1_only: false,
transport_profile: None,
};
let stream = self.inner.hub.open_local_stream(node_id, &meta)?;
let stream_id = stream.id;
@@ -98,7 +98,7 @@ pub(super) fn sample_plan(url: &str, provider_api_format: &str) -> ExecutionPlan
url: None,
extra: None,
}),
tls_profile: Some("chrome".to_string()),
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(10_000),
read_ms: Some(30_000),
+10 -15
View File
@@ -134,12 +134,11 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> anyhow::Resul
config.dns_cache_capacity,
));
// Build Hyper client for tunnel upstream requests (shared).
// DNS still flows through validated addresses from DnsCache, while the
// custom connector exposes per-request connect/TLS timing when available.
let upstream_client = upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_http1_client =
upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache));
let config = Arc::new(config);
// Build a profile-keyed Hyper client pool for tunnel upstream requests.
let upstream_client_pool =
upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache));
// Register with each Aether server and build per-server contexts.
// Wrapped in Arc<Mutex> so retry_failed_registrations can append later.
@@ -196,10 +195,9 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> anyhow::Resul
// Build shared application state
let tunnel_tls_config = Arc::new(crate::tunnel::client::build_tls_config());
let mut state = AppState {
config: Arc::new(config),
config,
dns_cache,
upstream_client,
upstream_http1_client,
upstream_client_pool,
tunnel_tls_config,
stream_gate: None,
distributed_stream_gate: None,
@@ -916,15 +914,12 @@ mod tests {
fn sample_state(config: Config) -> Arc<ProxyAppState> {
let config = Arc::new(config);
let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128));
let upstream_client =
upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_http1_client =
upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_client_pool =
upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache));
Arc::new(ProxyAppState {
config,
dns_cache,
upstream_client,
upstream_http1_client,
upstream_client_pool,
tunnel_tls_config: Arc::new(crate::tunnel::client::build_tls_config()),
stream_gate: None,
distributed_stream_gate: None,
+3 -5
View File
@@ -13,17 +13,15 @@ use crate::config::Config;
use crate::registration::client::AetherClient;
use crate::runtime::SharedDynamicConfig;
use crate::target_filter::DnsCache;
use crate::upstream_client::UpstreamClient;
use crate::upstream_client::UpstreamClientPool;
/// Central application state shared across all servers/tunnels.
pub struct AppState {
pub config: Arc<Config>,
/// DNS cache for upstream target resolution (shared).
pub dns_cache: Arc<DnsCache>,
/// Hyper client for tunnel upstream requests with validated DNS and connection timing.
pub upstream_client: UpstreamClient,
/// Dedicated Hyper client that forces HTTP/1.1 for upstreams that break on H2/ALPN.
pub upstream_http1_client: UpstreamClient,
/// Profile-keyed upstream client pool used by tunnel requests.
pub upstream_client_pool: UpstreamClientPool,
/// Shared TLS config for tunnel WebSocket connections (avoids re-parsing root CAs on each reconnect).
pub tunnel_tls_config: Arc<rustls::ClientConfig>,
/// Optional per-process stream admission gate.
+7 -6
View File
@@ -379,12 +379,16 @@ mod tests {
fn relay_probe_envelope() -> Vec<u8> {
let meta = protocol::RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: "http://127.0.0.1:80/blocked".to_string(),
headers: std::collections::HashMap::new(),
timeout: 5,
follow_redirects: None,
http1_only: false,
transport_profile: None,
};
let meta_json =
serde_json::to_vec(&meta).expect("tunnel relay probe metadata should serialize");
@@ -446,15 +450,12 @@ mod tests {
fn sample_state(config: Config) -> Arc<ProxyAppState> {
let config = Arc::new(config);
let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128));
let upstream_client =
upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_http1_client =
upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_client_pool =
upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache));
Arc::new(ProxyAppState {
config,
dns_cache,
upstream_client,
upstream_http1_client,
upstream_client_pool,
tunnel_tls_config: Arc::new(crate::tunnel::client::build_tls_config()),
stream_gate: None,
distributed_stream_gate: None,
+59 -34
View File
@@ -726,6 +726,7 @@ fn resolve_redirect<B>(
async fn execute_upstream_request(
state: &AppState,
server: &ServerContext,
meta: &RequestMeta,
current_url: &url::Url,
method: hyper::Method,
headers: &[(String, String)],
@@ -756,11 +757,14 @@ async fn execute_upstream_request(
}
let dns_ms = dns_start.elapsed().as_millis() as u64;
let client = if http1_only {
&state.upstream_http1_client
} else {
&state.upstream_client
};
let client_key = upstream_client::upstream_client_pool_key(
meta.provider_id.as_deref(),
meta.endpoint_id.as_deref(),
meta.key_id.as_deref(),
meta.transport_profile.as_ref(),
http1_only,
);
let client = state.upstream_client_pool.get_or_build(client_key)?;
let mut request = hyper::Request::builder()
.method(method)
@@ -1026,15 +1030,16 @@ async fn relay_upstream_response(
}
#[cfg(test)]
fn upstream_client_for_request<'a>(
state: &'a AppState,
fn upstream_client_pool_key_for_request(
meta: &RequestMeta,
) -> &'a upstream_client::UpstreamClient {
if meta.http1_only {
&state.upstream_http1_client
} else {
&state.upstream_client
}
) -> upstream_client::UpstreamClientPoolKey {
upstream_client::upstream_client_pool_key(
meta.provider_id.as_deref(),
meta.endpoint_id.as_deref(),
meta.key_id.as_deref(),
meta.transport_profile.as_ref(),
meta.http1_only,
)
}
/// Handle a single stream: receive body, execute upstream, send response.
@@ -1235,6 +1240,7 @@ async fn handle_stream_inner(
let response_ctx = match execute_upstream_request(
state,
server,
&meta,
&current_url,
current_method.clone(),
&current_headers,
@@ -1671,19 +1677,40 @@ mod tests {
#[test]
fn selects_http1_only_client_when_request_metadata_requires_it() {
let state = sample_state(None, None);
let default_meta = sample_request_meta();
assert!(std::ptr::eq(
upstream_client_for_request(state.as_ref(), &default_meta),
&state.upstream_client
));
assert_eq!(
upstream_client_pool_key_for_request(&default_meta).http_mode,
"auto"
);
let mut http1_meta = sample_request_meta();
http1_meta.http1_only = true;
assert!(std::ptr::eq(
upstream_client_for_request(state.as_ref(), &http1_meta),
&state.upstream_http1_client
));
assert_eq!(
upstream_client_pool_key_for_request(&http1_meta).http_mode,
"http1_only"
);
}
#[test]
fn upstream_client_pool_key_isolates_accounts() {
let mut first = sample_request_meta();
first.provider_id = Some("provider-1".to_string());
first.endpoint_id = Some("endpoint-1".to_string());
first.key_id = Some("key-1".to_string());
first.transport_profile = Some(aether_contracts::ResolvedTransportProfile {
profile_id: "profile-a".to_string(),
backend: "reqwest_rustls".to_string(),
http_mode: "auto".to_string(),
pool_scope: "key".to_string(),
extra: None,
});
let mut second = first.clone();
second.key_id = Some("key-2".to_string());
assert_ne!(
upstream_client_pool_key_for_request(&first),
upstream_client_pool_key_for_request(&second)
);
}
#[test]
@@ -2210,12 +2237,16 @@ mod tests {
fn sample_request_meta() -> RequestMeta {
RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: "https://example.com/ok".to_string(),
headers: HashMap::new(),
timeout: 30,
follow_redirects: None,
http1_only: false,
transport_profile: None,
}
}
@@ -2226,15 +2257,12 @@ mod tests {
ensure_rustls_provider();
let config = Arc::new(sample_config());
let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128));
let upstream_client =
upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_http1_client =
upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_client_pool =
upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache));
Arc::new(AppState {
config,
dns_cache,
upstream_client,
upstream_http1_client,
upstream_client_pool,
tunnel_tls_config: Arc::new(build_tls_config()),
stream_gate,
distributed_stream_gate,
@@ -2259,15 +2287,12 @@ mod tests {
fn sample_state_with_config(config: Config) -> Arc<AppState> {
let config = Arc::new(config);
let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128));
let upstream_client =
upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_http1_client =
upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache));
let upstream_client_pool =
upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache));
Arc::new(AppState {
config,
dns_cache,
upstream_client,
upstream_http1_client,
upstream_client_pool,
tunnel_tls_config: Arc::new(build_tls_config()),
stream_gate: None,
distributed_stream_gate: None,
+148 -11
View File
@@ -1,12 +1,18 @@
use std::collections::HashMap;
use std::convert::Infallible;
use std::future::Future;
use std::io;
use std::net::IpAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
use std::task::{Context, Poll};
use std::time::Duration;
use aether_contracts::{
ResolvedTransportProfile, TRANSPORT_BACKEND_HYPER_RUSTLS, TRANSPORT_BACKEND_REQWEST_RUSTLS,
TRANSPORT_HTTP_MODE_HTTP1_ONLY,
};
use bytes::Bytes;
use futures_util::Stream;
use http_body_util::combinators::UnsyncBoxBody;
@@ -37,6 +43,115 @@ type TlsStream = TokioIo<tokio_rustls::client::TlsStream<TcpStream>>;
pub type UpstreamRequestBody = UnsyncBoxBody<Bytes, io::Error>;
pub type UpstreamClient = Client<InstrumentedConnector, UpstreamRequestBody>;
const DEFAULT_PROFILE_ID: &str = "default";
const DEFAULT_BACKEND: &str = TRANSPORT_BACKEND_HYPER_RUSTLS;
const DEFAULT_HTTP_MODE: &str = "auto";
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct UpstreamClientPoolKey {
pub provider_id: String,
pub endpoint_id: String,
pub key_id: String,
pub profile_id: String,
pub backend: String,
pub http_mode: String,
}
#[derive(Clone)]
pub struct UpstreamClientPool {
config: Arc<Config>,
dns_cache: Arc<DnsCache>,
clients: Arc<Mutex<HashMap<UpstreamClientPoolKey, UpstreamClient>>>,
}
impl UpstreamClientPool {
pub fn new(config: Arc<Config>, dns_cache: Arc<DnsCache>) -> Self {
Self {
config,
dns_cache,
clients: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn get_or_build(&self, key: UpstreamClientPoolKey) -> Result<UpstreamClient, String> {
if let Some(client) = self
.clients
.lock()
.expect("client pool lock")
.get(&key)
.cloned()
{
return Ok(client);
}
validate_proxy_transport_backend(&key.backend)?;
let http1_only = key
.http_mode
.eq_ignore_ascii_case(TRANSPORT_HTTP_MODE_HTTP1_ONLY);
let client = build_upstream_client_with_protocol(
&self.config,
Arc::clone(&self.dns_cache),
http1_only,
);
self.clients
.lock()
.expect("client pool lock")
.insert(key, client.clone());
Ok(client)
}
}
pub fn upstream_client_pool_key(
provider_id: Option<&str>,
endpoint_id: Option<&str>,
key_id: Option<&str>,
profile: Option<&ResolvedTransportProfile>,
http1_only: bool,
) -> UpstreamClientPoolKey {
let profile_http_mode = profile
.map(|profile| profile.http_mode.trim())
.filter(|value| !value.is_empty())
.unwrap_or(DEFAULT_HTTP_MODE);
let http_mode = if http1_only {
TRANSPORT_HTTP_MODE_HTTP1_ONLY
} else {
profile_http_mode
};
UpstreamClientPoolKey {
provider_id: normalized_pool_key_part(provider_id),
endpoint_id: normalized_pool_key_part(endpoint_id),
key_id: normalized_pool_key_part(key_id),
profile_id: profile
.map(|profile| profile.profile_id.trim())
.filter(|value| !value.is_empty())
.unwrap_or(DEFAULT_PROFILE_ID)
.to_string(),
backend: profile
.map(|profile| profile.backend.trim())
.filter(|value| !value.is_empty())
.unwrap_or(DEFAULT_BACKEND)
.to_string(),
http_mode: http_mode.to_string(),
}
}
fn normalized_pool_key_part(value: Option<&str>) -> String {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("-")
.to_string()
}
fn validate_proxy_transport_backend(backend: &str) -> Result<(), String> {
if backend.eq_ignore_ascii_case(TRANSPORT_BACKEND_HYPER_RUSTLS)
|| backend.eq_ignore_ascii_case(TRANSPORT_BACKEND_REQWEST_RUSTLS)
{
return Ok(());
}
Err(format!("unsupported transport profile backend: {backend}"))
}
pub fn stream_request_body<S>(stream: S) -> UpstreamRequestBody
where
S: Stream<Item = Result<Frame<Bytes>, io::Error>> + Send + 'static,
@@ -181,17 +296,6 @@ impl Service<Uri> for InstrumentedConnector {
}
}
pub fn build_upstream_client(config: &Config, dns_cache: Arc<DnsCache>) -> UpstreamClient {
build_upstream_client_with_protocol(config, dns_cache, false)
}
pub fn build_http1_only_upstream_client(
config: &Config,
dns_cache: Arc<DnsCache>,
) -> UpstreamClient {
build_upstream_client_with_protocol(config, dns_cache, true)
}
fn build_upstream_client_with_protocol(
config: &Config,
dns_cache: Arc<DnsCache>,
@@ -424,6 +528,7 @@ impl rt::Write for MaybeHttpsStream {
#[cfg(test)]
mod tests {
use super::*;
use aether_contracts::ResolvedTransportProfile;
use hyper::Response;
#[test]
@@ -476,4 +581,36 @@ mod tests {
assert_eq!(timing.response_wait_ms, 320);
assert!(!timing.connection_reused);
}
#[test]
fn upstream_client_pool_key_includes_profile_identity() {
let profile = ResolvedTransportProfile {
profile_id: "profile-a".to_string(),
backend: TRANSPORT_BACKEND_REQWEST_RUSTLS.to_string(),
http_mode: "auto".to_string(),
pool_scope: "key".to_string(),
extra: None,
};
let pool_key = upstream_client_pool_key(
Some("provider-1"),
Some("endpoint-1"),
Some("key-1"),
Some(&profile),
false,
);
assert_eq!(pool_key.provider_id, "provider-1");
assert_eq!(pool_key.endpoint_id, "endpoint-1");
assert_eq!(pool_key.key_id, "key-1");
assert_eq!(pool_key.profile_id, "profile-a");
assert_eq!(pool_key.backend, TRANSPORT_BACKEND_REQWEST_RUSTLS);
assert_eq!(pool_key.http_mode, "auto");
}
#[test]
fn upstream_client_pool_rejects_unsupported_backend() {
let error = validate_proxy_transport_backend("utls").unwrap_err();
assert!(error.contains("unsupported transport profile backend"));
}
}