This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+256
View File
@@ -0,0 +1,256 @@
{
let websocket_upstream_h2 = !websocket_legacy_support
&& handshake.as_ref().is_some_and(|handshake| handshake.h2())
&& backend.protocol == crate::config::UpstreamProtocol::Http2;
if let Some(handshake) = &handshake {
request.headers_mut().remove("content-length");
request.headers_mut().remove("transfer-encoding");
if websocket_upstream_h2 {
// Preserve CONNECT + :protocol=websocket for an explicit HTTP/2
// upstream. HTTP/1.1-only fields are forbidden on this hop.
request.headers_mut().remove("connection");
request.headers_mut().remove("upgrade");
request.headers_mut().remove("sec-websocket-key");
request.headers_mut().remove("sec-websocket-accept");
} else {
request
.headers_mut()
.insert("connection", HeaderValue::from_static("Upgrade"));
request
.headers_mut()
.insert("upgrade", HeaderValue::from_static("websocket"));
if !handshake.h1() {
// Extended CONNECT replaces the RFC 6455 proof headers on the
// downstream hop. Synthesize them only for an HTTP/1.1 backend.
*request.method_mut() = Method::GET;
request.headers_mut().remove("sec-websocket-key");
request.headers_mut().remove("sec-websocket-accept");
if let Some(key) = handshake.upstream_key() {
if let Ok(key) = HeaderValue::from_str(key) {
request.headers_mut().insert("sec-websocket-key", key);
} else {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
"RustProxy: failed to build WebSocket handshake\n",
);
}
}
}
}
}
if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
// Enforce after all custom request-header rules; they cannot restore credentials.
auth.apply_upstream(request.headers_mut(), basic_identity);
}
// Transformation operates on the identity representation. Prefer identity from
// the origin, while still decoding a supported encoding if the origin ignores it.
if range_requested || transform_settings.is_some_and(|settings| settings.enabled) {
request.headers_mut().remove("accept-encoding");
}
let mut revalidation_sent = false;
if pending_revalidation {
if let Some(cached) = &cached_revalidation {
revalidation_sent = cached.apply_validators(request.headers_mut());
}
}
// Varnish hashes HEAD with GET and turns a regular cacheable backend fetch
// into GET. The outer response middleware still strips the body for the
// original HEAD client, while a MISS can populate the shared GET object.
if request.method() == Method::HEAD && cache_ticket.is_some() {
*request.method_mut() = Method::GET;
}
*request.uri_mut() = uri.clone();
let upstream_version = if websocket_legacy_support && !websocket_upstream_h2 {
Version::HTTP_11
} else {
upstream_request_version(&backend, websocket_upstream_h2)
};
if normalize_h3_h1_unknown_body(&mut request, upstream_version)
.await
.is_err()
{
return error(
StatusCode::BAD_REQUEST,
"RustProxy: invalid or incomplete request body\n",
);
}
*request.version_mut() = upstream_version;
let mut h2_direct_session = None;
if websocket_upstream_h2 {
let Some(exchange) = exchange.clone() else {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
"RustProxy: WebSocket exchange state unavailable\n",
);
};
let stop_token = stop
.as_ref()
.map(|value| value.0.clone())
.unwrap_or_else(|| state.shutdown.clone());
let session = state.tunnel_registry.register_pending(
exchange.clone(),
backend.origin.clone(),
"h2-rfc8441-h2-upstream",
stop_token,
lease.clone(),
);
websocket::spawn_h2_idle_watch(state.clone(), &session);
let body = std::mem::replace(request.body_mut(), Body::empty());
*request.body_mut() = websocket::wrap_h2_request_body(body, session.clone(), exchange);
h2_direct_session = Some(session);
}
// Do not classify a slow client upload as a slow origin response. Probes
// independently measure origin liveness/latency with body-free requests.
let request_body_empty = request.body().size_hint().exact() == Some(0);
let proactive_base_headers = request.headers().clone();
let response_header_timeout = frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.unwrap_or(state.header_timeout);
let retry_headers = request.headers().clone();
let retry_method = request.method().clone();
let expected_backend_host = if backend.preserve_host {
&original_host
} else {
&backend.host_header
};
let retry_host_follows_backend = request
.headers()
.get(HOST)
.is_some_and(|value| value == expected_backend_host);
let retry_policy = route.pool.as_ref().map(|pool| pool.settings.retry.clone());
let retry_enabled = route.backend_id.is_none()
&& !admin_control_plane
&& handshake.is_none()
&& request_body_empty
&& retry_safe_method(&retry_method)
&& retry_policy.as_ref().is_some_and(|policy| policy.max_retries > 0);
let upstream_started = Instant::now();
let mut attempt_started = Instant::now();
let mut active_backend = backend.clone();
let mut active_lease = backend_lease.clone();
drop(backend_lease);
let mut active_affinity_cookie = affinity_cookie.clone();
let mut active_uri = uri.clone();
let mut request_attempt = Some(request);
let mut attempted_backends = Vec::<String>::new();
let mut retries = 0u8;
let upstream_result = loop {
let client = if handshake.is_some() && !websocket_upstream_h2 {
&active_backend.websocket_client
} else {
&active_backend.client
};
let result = tokio::time::timeout(
response_header_timeout,
client.request(request_attempt.take().expect("upstream request attempt missing")),
)
.await;
let attempt_elapsed = attempt_started.elapsed();
let Some(policy) = retry_policy.as_ref() else {
break result;
};
let retry_reason = match &result {
Ok(Ok(response)) if policy.retries_status(response.status().as_u16()) => {
Some("http_status")
}
Ok(Err(cause)) if cause.is_connect() && policy.retry_connect_errors => Some("connect"),
Err(_) if policy.retry_timeouts => Some("timeout"),
_ => None,
};
if !retry_enabled || retries >= policy.max_retries || retry_reason.is_none() {
break result;
}
attempted_backends.push(active_backend.id.clone());
let Some(pool) = route.pool.as_ref() else {
break result;
};
let selection = match pool
.select_retry_wait(
context.client_ip,
&retry_headers,
tls,
&attempted_backends,
)
.await
{
Ok(selection) => selection,
Err(crate::balance::SelectionFailure::Capacity) => {
tracing::debug!(
vhost = %pool.id,
attempt = retries,
"retry stopped because all remaining backends are at concurrency capacity"
);
break result;
}
Err(crate::balance::SelectionFailure::Unavailable) => break result,
};
let next_backend = selection.lease.member.backend.clone();
let Some(next_uri) = upstream_uri(&next_backend, &origin_suffix) else {
break result;
};
let Some(next_request) = retry_request(
&retry_method,
upstream_request_version(&next_backend, false),
&retry_headers,
&next_backend,
&original_host,
&origin_suffix,
retry_host_follows_backend,
) else {
break result;
};
if let Some(lease) = &active_lease {
match &result {
Ok(Ok(response)) => {
lease.response(response.status().as_u16(), attempt_elapsed, true)
}
Ok(Err(cause)) => {
let connect_failure = cause.is_connect();
let detail = upstream_failure_detail(
cause,
&active_backend,
attempt_elapsed,
connect_failure,
);
lease.error_detail(
upstream_error_class(cause, connect_failure),
&detail,
attempt_elapsed,
);
}
Err(_) => lease.error_detail(
"response_header_timeout",
"response_header_timeout",
attempt_elapsed,
),
}
lease.retry_attempted();
}
retries = retries.saturating_add(1);
selection.lease.started();
if let Some(exchange) = &exchange {
*exchange.backend.lock().unwrap_or_else(|e| e.into_inner()) =
Some(selection.lease.clone());
}
tracing::info!(
previous_backend = %active_backend.id,
backend = %next_backend.id,
attempt = retries,
reason = retry_reason.unwrap_or("unknown"),
"retrying replay-safe request on another backend"
);
active_backend = next_backend;
active_lease = Some(selection.lease);
active_affinity_cookie = selection.cookie;
active_uri = next_uri;
request_attempt = Some(next_request);
attempt_started = Instant::now();
};
let final_attempt_elapsed = attempt_started.elapsed();
(
websocket_upstream_h2, revalidation_sent, h2_direct_session, request_body_empty,
proactive_base_headers, upstream_started, active_backend,
active_lease, active_affinity_cookie, active_uri, retries, upstream_result,
final_attempt_elapsed,
)
}