use bytes::Bytes;
use h2::server::{self, SendResponse};
use http::{Request, Response, StatusCode};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use tracing::{debug, error, info, warn};
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::server::http_common::handler::{RequestData, RequestFilter};
use crate::server::Http2Protocol;
use crate::state::app_state::AppState;
use super::actions::HTTP2_REQUEST_EVENT;
use super::push::PendingPush;
pub const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(30);
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
pub const IDLE_BETWEEN_REQUESTS_TIMEOUT: Duration = Duration::from_secs(300);
const GOAWAY_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
const MAX_CONNECTIONS: usize = 128;
pub const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
pub const MAX_CONCURRENT_STREAMS: u32 = 100;
pub const INITIAL_STREAM_WINDOW_BYTES: u32 = 65_535;
pub const INITIAL_CONNECTION_WINDOW_BYTES: u32 = 1024 * 1024;
pub const MAX_FRAME_BYTES: u32 = 16_384;
pub const MAX_HEADER_LIST_BYTES: u32 = 32 * 1024;
pub const CONNECTION_BODY_BUDGET_BYTES: usize = crate::server::http_common::MAX_REQUEST_BODY_BYTES;
pub fn bounded_h2_builder() -> server::Builder {
let mut builder = server::Builder::new();
builder
.max_concurrent_streams(MAX_CONCURRENT_STREAMS)
.initial_window_size(INITIAL_STREAM_WINDOW_BYTES)
.initial_connection_window_size(INITIAL_CONNECTION_WINDOW_BYTES)
.max_frame_size(MAX_FRAME_BYTES)
.max_header_list_size(MAX_HEADER_LIST_BYTES);
builder
}
#[derive(Clone, Default)]
pub struct BodyBudget(Arc<std::sync::atomic::AtomicUsize>);
impl BodyBudget {
pub fn new() -> Self {
Self::default()
}
pub fn in_use(&self) -> usize {
self.0.load(std::sync::atomic::Ordering::SeqCst)
}
fn reservation(&self) -> BodyReservation {
BodyReservation {
budget: self.clone(),
held: 0,
}
}
}
struct BodyReservation {
budget: BodyBudget,
held: usize,
}
impl BodyReservation {
fn grow(&mut self, n: usize) -> bool {
use std::sync::atomic::Ordering;
let taken = self
.budget
.0
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |used| {
used.checked_add(n)
.filter(|&total| total <= CONNECTION_BODY_BUDGET_BYTES)
})
.is_ok();
if taken {
self.held += n;
}
taken
}
}
impl Drop for BodyReservation {
fn drop(&mut self) {
self.budget
.0
.fetch_sub(self.held, std::sync::atomic::Ordering::SeqCst);
}
}
#[derive(Clone, Copy, Debug)]
pub struct H2Bounds {
pub first_byte: Duration,
pub idle: Duration,
}
impl H2Bounds {
pub fn from_secs(first_byte_secs: Option<u64>, idle_secs: Option<u64>) -> Self {
Self {
first_byte: first_byte_secs
.map(Duration::from_secs)
.unwrap_or(FIRST_BYTE_READ_TIMEOUT),
idle: idle_secs
.map(Duration::from_secs)
.unwrap_or(IDLE_BETWEEN_REQUESTS_TIMEOUT),
}
}
}
pub struct H2Server;
impl H2Server {
pub async fn spawn_with_push_support(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
tls_config: Option<Arc<rustls::ServerConfig>>,
bounds: H2Bounds,
) -> anyhow::Result<SocketAddr> {
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
let protocol_name = if tls_config.is_some() {
"HTTP/2 (TLS, h2 with push)"
} else {
"HTTP/2 (h2c with push)"
};
info!("{} server listening on {}", protocol_name, local_addr);
let _ = status_tx.send(format!(
"[INFO] {} server listening on {}",
protocol_name, local_addr
));
let protocol = Arc::new(Http2Protocol::new());
let filter = Arc::new(RequestFilter::from_startup_params(
app_state
.get_server(server_id)
.await
.and_then(|s| s.startup_params)
.as_ref(),
));
for warning in filter.warnings() {
let _ = status_tx.send(format!("[ERROR] HTTP/2 request_filter: {}", warning));
}
let tls_acceptor = tls_config.map(|config| tokio_rustls::TlsAcceptor::from(config));
let refusal: &'static [u8] = if tls_acceptor.is_some() {
b""
} else {
CONNECTION_CAP_REFUSAL
};
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let task_registrar = app_state.clone();
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
refusal,
"HTTP/2",
Some(&status_tx),
)
.await
{
Ok((tcp_stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = tcp_stream.local_addr().unwrap_or(local_addr);
info!(
"Accepted {} connection {} from {}",
protocol_name, connection_id, remote_addr
);
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
"recent_requests": []
})),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let tls_acceptor_clone = tls_acceptor.clone();
let filter_clone = filter.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
match tokio::time::timeout(
bounds.first_byte,
tcp_stream.peek(&mut [0u8; 1]),
)
.await
{
Ok(Ok(0)) | Ok(Err(_)) => {
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
return;
}
Ok(Ok(_)) => {}
Err(_) => {
debug!(
"{} connection {} sent nothing for {}s; closing \
before the preface",
protocol_name,
connection_id,
bounds.first_byte.as_secs()
);
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
return;
}
}
let result: Result<(), Box<dyn std::error::Error + Send + Sync>> =
if let Some(acceptor) = tls_acceptor_clone {
match tokio::time::timeout(
HANDSHAKE_TIMEOUT,
acceptor.accept(tcp_stream),
)
.await
.unwrap_or_else(|_| {
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"TLS handshake did not complete in time",
))
}) {
Ok(tls_stream) => {
debug!(
"{} TLS handshake complete with {}",
protocol_name, remote_addr
);
let _ = status_tx_clone.send(format!(
"[DEBUG] {} TLS handshake complete with {}",
protocol_name, remote_addr
));
handle_h2_connection(
tls_stream,
connection_id,
server_id,
llm_client_clone,
app_state_clone.clone(),
status_tx_clone.clone(),
protocol_clone,
filter_clone,
bounds.idle,
)
.await
}
Err(e) => {
error!(
"{} TLS handshake failed: {}",
protocol_name, e
);
let _ = status_tx_clone.send(format!(
"[ERROR] {} TLS handshake failed: {}",
protocol_name, e
));
Err(Box::new(e))
}
}
} else {
handle_h2_connection(
tcp_stream,
connection_id,
server_id,
llm_client_clone,
app_state_clone.clone(),
status_tx_clone.clone(),
protocol_clone,
filter_clone,
bounds.idle,
)
.await
};
if let Err(e) = result {
error!("{} connection error: {}", protocol_name, e);
}
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx_clone.send(format!(
"✗ {} connection {connection_id} closed",
protocol_name
));
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
})
.await;
}
Err(e) => {
error!("Failed to accept HTTP/2 connection: {}", e);
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_h2_connection<T>(
tcp_stream: T,
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<Http2Protocol>,
filter: Arc<RequestFilter>,
idle_timeout: Duration,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let mut h2_conn = match tokio::time::timeout(
HANDSHAKE_TIMEOUT,
bounded_h2_builder().handshake(tcp_stream),
)
.await
{
Ok(conn) => conn?,
Err(_) => {
debug!(
"HTTP/2 connection {} did not complete the preface in {}s; closing",
connection_id,
HANDSHAKE_TIMEOUT.as_secs()
);
return Ok(());
}
};
debug!("HTTP/2 handshake complete for connection {}", connection_id);
let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
let body_budget = BodyBudget::new();
loop {
let next = tokio::select! {
next = h2_conn.accept() => next,
_ = crate::server::accept_bounded::watch_idle(Arc::clone(&activity), idle_timeout) => {
debug!(
"HTTP/2 connection {} carried no request for {}s; sending GOAWAY",
connection_id,
idle_timeout.as_secs()
);
h2_conn.graceful_shutdown();
let _ = tokio::time::timeout(GOAWAY_DRAIN_TIMEOUT, async {
while let Some(_stream) = h2_conn.accept().await {}
})
.await;
return Ok(());
}
};
let Some(result) = next else { break };
let (request, send_response) = result?;
activity.touch();
let llm_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let filter_clone = filter.clone();
let busy = activity.busy();
let stream_budget = body_budget.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _busy = busy;
if let Err(e) = handle_h2_request(
request,
send_response,
connection_id,
server_id,
llm_clone,
app_state_clone,
status_clone,
protocol_clone,
filter_clone,
stream_budget,
)
.await
{
error!("Error handling HTTP/2 request: {}", e);
}
})
.await;
}
Ok(())
}
fn build_h2_response_head(
status: u16,
headers: impl IntoIterator<Item = (String, String)>,
context: &str,
) -> Response<()> {
let status_code = StatusCode::from_u16(status).unwrap_or_else(|_| {
error!(
"{}: invalid HTTP status {} (must be 100-599), sending 500 instead",
context, status
);
StatusCode::INTERNAL_SERVER_ERROR
});
let mut builder = Response::builder().status(status_code);
for (name, value) in headers {
match (
http::header::HeaderName::from_bytes(name.as_bytes()),
http::header::HeaderValue::from_str(&value),
) {
(Ok(n), Ok(v)) => builder = builder.header(n, v),
_ => warn!("{}: dropping invalid response header {:?}", context, name),
}
}
builder.body(()).unwrap_or_else(|e| {
error!(
"{}: failed to build response ({}), sending bare 500",
context, e
);
let mut fallback = Response::new(());
*fallback.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
fallback
})
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_h2_request(
request: Request<h2::RecvStream>,
mut send_response: SendResponse<Bytes>,
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<Http2Protocol>,
filter: Arc<RequestFilter>,
body_budget: BodyBudget,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let method = request.method().to_string();
let uri = request
.uri()
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or(request.uri().path())
.to_string();
let version = format!("{:?}", request.version());
let mut headers = HashMap::new();
for (name, value) in request.headers() {
if let Ok(value_str) = value.to_str() {
headers.insert(name.to_string(), value_str.to_string());
}
}
let mut body_stream = request.into_body();
let mut body_bytes = Vec::new();
let mut body_too_large = false;
let mut reservation = body_budget.reservation();
let mut over_connection_budget = false;
loop {
match body_stream.data().await {
Some(Ok(chunk)) => {
if body_bytes.len() + chunk.len()
> crate::server::http_common::MAX_REQUEST_BODY_BYTES
{
body_too_large = true;
break;
}
if !reservation.grow(chunk.len()) {
over_connection_budget = true;
break;
}
body_bytes.extend_from_slice(&chunk);
let _ = body_stream.flow_control().release_capacity(chunk.len());
}
Some(Err(e)) => {
warn!("Error reading request body: {}", e);
let _ = status_tx.send(format!("[WARN] Error reading body: {}", e));
break;
}
None => {
break;
}
}
}
if over_connection_budget {
let held_by_others = body_budget.in_use().saturating_sub(body_bytes.len());
drop(body_bytes);
drop(reservation);
warn!(
"HTTP/2 {} {} decision=refused_connection_body_budget: the connection's other \
streams hold {} of {} body bytes",
method, uri, held_by_others, CONNECTION_BODY_BUDGET_BYTES
);
let _ = status_tx.send(format!("→ HTTP/2 {} {} → 503", method, uri));
let body = "Service Unavailable: this connection is already buffering as many request \
bodies as it may; retry when an earlier request has been answered\n";
let response = build_h2_response_head(
503,
[
("content-type".to_string(), "text/plain".to_string()),
("retry-after".to_string(), "1".to_string()),
],
"HTTP/2 connection body budget",
);
let mut stream = send_response.send_response(response, false)?;
stream.send_data(Bytes::from(body), true)?;
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(body.len() as u64),
None,
Some(1),
)
.await;
return Ok(());
}
if body_too_large {
warn!(
"HTTP/2 {} {} decision=refused_body_too_large: over {} bytes",
method,
uri,
crate::server::http_common::MAX_REQUEST_BODY_BYTES
);
let _ = status_tx.send(format!("→ HTTP/2 {} {} → 413", method, uri));
let body = format!(
"Payload Too Large: request bodies are limited to {} bytes\n",
crate::server::http_common::MAX_REQUEST_BODY_BYTES
);
let body_len = body.len() as u64;
let response = build_h2_response_head(
413,
[("content-type".to_string(), "text/plain".to_string())],
"HTTP/2 payload too large",
);
let mut stream = send_response.send_response(response, false)?;
stream.send_data(Bytes::from(body), true)?;
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(body_len),
None,
Some(1),
)
.await;
return Ok(());
}
app_state
.update_connection_stats(
server_id,
connection_id,
Some(body_bytes.len() as u64),
None,
Some(1),
None,
)
.await;
debug!(
"HTTP/2 request: {} {} {} ({} bytes) from {:?}",
method,
uri,
version,
body_bytes.len(),
connection_id
);
let _ = status_tx.send(format!(
"[DEBUG] HTTP/2 request: {} {} {} ({} bytes)",
method,
uri,
version,
body_bytes.len()
));
let path = uri.split('?').next().unwrap_or(&uri).to_string();
if !filter.is_pass_through() {
let request_data = RequestData {
method: method.clone(),
uri: uri.clone(),
version: version.clone(),
headers: headers.clone(),
body_bytes: Bytes::from(body_bytes.clone()),
};
if !filter.allows(&request_data, &path) {
let (status, hdrs, body) = filter.rejection_parts();
let _ = status_tx.send(format!(
"↩ HTTP/2 filtered {} {} → {} (no LLM call)",
method, path, status
));
let body_len = body.len() as u64;
let response = build_h2_response_head(status, hdrs, "HTTP/2 filtered_response");
let mut stream = send_response.send_response(response, false)?;
stream.send_data(Bytes::from(body), true)?;
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(body_len),
None,
Some(1),
)
.await;
return Ok(());
}
}
let body_is_binary = std::str::from_utf8(&body_bytes).is_err();
let body_text = String::from_utf8_lossy(&body_bytes);
let mut event_data = serde_json::json!({
"method": method,
"uri": uri,
"version": version,
"headers": headers,
"body": if body_text.is_empty() { "" } else { body_text.as_ref() },
"body_bytes": body_bytes.len()
});
if body_is_binary {
event_data["body_is_binary"] = serde_json::Value::Bool(true);
}
let event = Event::new(&HTTP2_REQUEST_EVENT, event_data);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
debug!("LLM HTTP/2 response received");
for msg in execution_result.messages {
let _ = status_tx.send(msg);
}
let mut status_code = 200;
let mut response_headers = HashMap::new();
let mut response_body = String::new();
let mut pushes = Vec::new();
let mut produced_response = false;
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Output(output_data) => {
if let Ok(json_value) =
serde_json::from_slice::<serde_json::Value>(&output_data)
{
if json_value
.get("_push_directive")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let push = PendingPush {
path: json_value
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string(),
method: json_value
.get("method")
.and_then(|v| v.as_str())
.unwrap_or("GET")
.to_string(),
status: json_value
.get("status")
.and_then(|v| v.as_u64())
.unwrap_or(200)
as u16,
headers: json_value
.get("headers")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| {
v.as_str().map(|s| (k.clone(), s.to_string()))
})
.collect()
})
.unwrap_or_default(),
body: json_value
.get("body")
.and_then(|v| v.as_str())
.unwrap_or("")
.as_bytes()
.to_vec(),
};
pushes.push(push);
} else {
if let Some(status) =
json_value.get("status").and_then(|v| v.as_u64())
{
status_code = status as u16;
produced_response = true;
}
if let Some(headers_obj) =
json_value.get("headers").and_then(|v| v.as_object())
{
for (k, v) in headers_obj {
if let Some(v_str) = v.as_str() {
response_headers.insert(k.clone(), v_str.to_string());
produced_response = true;
}
}
}
if let Some(body) = json_value.get("body").and_then(|v| v.as_str())
{
response_body = body.to_string();
produced_response = true;
}
}
}
}
_ => {}
}
}
for push in pushes {
debug!("Executing server push for {}", push.path);
let push_body_len = push.body.len();
let mut push_request = http::Request::builder()
.method(push.method.as_str())
.uri(&push.path);
for (name, value) in &push.headers {
push_request = push_request.header(name, value);
}
if let Ok(push_req) = push_request.body(()) {
match send_response.push_request(push_req) {
Ok(mut push_stream) => {
let mut push_response = http::Response::builder().status(push.status);
for (name, value) in &push.headers {
push_response = push_response.header(name, value);
}
if let Ok(push_resp) = push_response.body(()) {
match push_stream.send_response(push_resp, false) {
Ok(mut stream) => {
if let Err(e) =
stream.send_data(Bytes::from(push.body), true)
{
warn!(
"Failed to send push body for {}: {}",
push.path, e
);
} else {
debug!("Successfully pushed {}", push.path);
let _ = status_tx.send(format!(
"⬆ Pushed {} ({} bytes)",
push.path, push_body_len
));
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(push_body_len as u64),
None,
Some(1),
)
.await;
}
}
Err(e) => {
warn!(
"Failed to send push response for {}: {}",
push.path, e
);
}
}
}
}
Err(e) => {
warn!("Client rejected push for {}: {}", push.path, e);
}
}
}
}
if !produced_response {
if let Some((status, hdrs, body)) = filter.default_response_parts() {
warn!(
"HTTP/2 {} {} decision=default_response: actions ran but none \
yielded a response; answering the configured default ({})",
method, uri, status
);
let _ = status_tx.send(format!(
"→ HTTP/2 {} {} → {} (configured default_response)",
method, uri, status
));
let body_len = body.len() as u64;
let response = build_h2_response_head(status, hdrs, "HTTP/2 default_response");
let mut stream = send_response.send_response(response, false)?;
stream.send_data(Bytes::from(body), true)?;
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(body_len),
None,
Some(1),
)
.await;
return Ok(());
}
warn!(
"HTTP/2 {} {} decision=fail_closed_no_action: actions ran but none \
yielded a response",
method, uri
);
let _ = status_tx.send(format!(
"✗ HTTP/2 {} {} → 500 (no response action produced)",
method, uri
));
let body = crate::utils::WireFailure::Unavailable.prefixed_text();
let response = Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(())?;
let mut stream = send_response.send_response(response, false)?;
stream.send_data(Bytes::from(body), true)?;
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(body.len() as u64),
None,
Some(1),
)
.await;
return Ok(());
}
let _ = status_tx.send(format!(
"→ HTTP/2 {} {} → {} ({} bytes)",
method,
uri,
status_code,
response_body.len()
));
let body_len = response_body.len() as u64;
let response = build_h2_response_head(status_code, response_headers, "HTTP/2");
let mut stream = send_response.send_response(response, false)?;
stream.send_data(Bytes::from(response_body), true)?;
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(body_len),
None,
Some(1),
)
.await;
}
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
let (status, decision) = match failure {
crate::utils::WireFailure::Overloaded => (
StatusCode::SERVICE_UNAVAILABLE,
"fail_closed_llm_overloaded",
),
crate::utils::WireFailure::Unavailable => {
(StatusCode::INTERNAL_SERVER_ERROR, "fail_closed_llm_error")
}
};
error!(
"HTTP/2 {} {} decision={} status={}: {}",
method,
uri,
decision,
status.as_u16(),
e
);
let _ = status_tx.send(format!(
"✗ LLM error for {} {} → {}: {}",
method,
uri,
status.as_u16(),
e
));
let error_body = failure.prefixed_text();
let mut builder = Response::builder().status(status);
if failure.is_overloaded() {
builder = builder.header(http::header::RETRY_AFTER, "1");
}
let response = builder.body(())?;
let mut stream = send_response.send_response(response, false)?;
stream.send_data(Bytes::from(error_body), true)?;
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(error_body.len() as u64),
None,
Some(1),
)
.await;
}
}
Ok(())
}