Fix opening native targets while they are still building and retry tcp connections to avoid server is still loading page (#5358)

* retry tcp connections

* don't open native binaries that don't exist

* fix formatting

* fix proxy
This commit is contained in:
Evan Almloff authored and GitHub committed 2026-03-13 13:01:42 -05:00
1 parent f472349012
commit 77a50f916f
2 files changed
+91 -51

No files matched your search

+66 -37
View File
@@ -2,7 +2,7 @@ use crate::config::WebProxyConfig;
use crate::TraceSrc;
use crate::{Error, Result};
use anyhow::{bail, Context};
use anyhow::bail;
use axum::body::Body;
use axum::http::request::Parts;
use axum::{body::Body as MyBody, response::IntoResponse};
@@ -11,44 +11,65 @@ use axum::{
routing::{any, MethodRouter},
Router,
};
use hyper::client::conn::http1;
use hyper::header::*;
use hyper::{Request, Response, Uri};
use hyper_util::{
client::legacy::{self, connect::HttpConnector},
rt::TokioExecutor,
};
use hyper_util::rt::TokioIo;
use tokio::net::TcpStream;
#[derive(Debug, Clone)]
struct ProxyClient {
inner: legacy::Client<hyper_rustls::HttpsConnector<HttpConnector>, MyBody>,
url: Uri,
}
/// Establish a TCP connection to the backend with retry, then send the HTTP request.
/// This reuses the same TCP connection for both health check and request,
/// and supports streaming request bodies (no buffering).
async fn send_with_retry(
url: &Uri,
req: Request<MyBody>,
handle_error: fn(Error) -> Response<Body>,
) -> std::result::Result<Response<hyper::body::Incoming>, Response<Body>> {
let host = url.host().unwrap_or("127.0.0.1");
let port = url.port_u16().unwrap_or(80);
let addr = format!("{host}:{port}");
impl ProxyClient {
fn new(url: Uri) -> Self {
let _ = rustls::crypto::ring::default_provider().install_default();
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.unwrap()
.https_or_http()
.enable_all_versions()
.build();
Self {
inner: legacy::Client::builder(TokioExecutor::new()).build(https),
url,
let mut backoff = std::time::Duration::from_millis(100);
let max_wait = std::time::Duration::from_secs(30);
let start = std::time::Instant::now();
// Retry TCP connect until backend is ready
let stream = loop {
match TcpStream::connect(&addr).await {
Ok(stream) => break stream,
Err(e) => {
if start.elapsed() >= max_wait {
return Err(handle_error(anyhow::anyhow!(
"Backend not ready after {max_wait:?}: {e}"
)));
}
tracing::debug!("Backend not ready, retrying in {backoff:?}...");
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(std::time::Duration::from_secs(2));
}
}
}
};
async fn send(&self, mut req: Request<MyBody>) -> Result<Response<hyper::body::Incoming>> {
let mut uri_parts = req.uri().clone().into_parts();
uri_parts.authority = self.url.authority().cloned();
uri_parts.scheme = self.url.scheme().cloned();
*req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?;
self.inner
.request(req)
.await
.context("Failed to send proxy request")
}
// Wrap the TCP stream for hyper
let io = TokioIo::new(stream);
// Perform HTTP/1.1 handshake on the same connection
let (mut sender, conn) = http1::handshake(io)
.await
.map_err(|e| handle_error(anyhow::anyhow!("HTTP handshake failed: {e}")))?;
// Spawn connection driver to keep it alive
tokio::spawn(async move {
if let Err(e) = conn.await {
tracing::debug!("Connection closed: {e}");
}
});
// Send request through the established connection (streaming body)
sender
.send_request(req)
.await
.map_err(|e| handle_error(anyhow::anyhow!("Request failed: {e}")))
}
/// Add routes to the router handling the specified proxy config.
@@ -99,8 +120,6 @@ pub(crate) fn proxy_to(
nocache: bool,
handle_error: fn(Error) -> Response<Body>,
) -> MethodRouter {
let client = ProxyClient::new(url.clone());
any(move |parts: Parts, mut req: Request<MyBody>| async move {
// Prevent request loops
if req.headers().get("x-proxied-by-dioxus").is_some() {
@@ -131,9 +150,19 @@ pub(crate) fn proxy_to(
let uri = req.uri().clone();
// retry with backoff
// Set Host header for backend (send_with_retry handles TCP connection via url)
if let Some(authority) = url.authority() {
req.headers_mut().insert(
HOST,
authority
.to_string()
.parse()
.expect("authority is valid header value"),
);
}
let res = client.send(req).await.map_err(handle_error);
// Send with retry - TCP connect retries, then reuses connection for HTTP
let res = send_with_retry(&url, req, handle_error).await;
match res {
Ok(res) => {
+25 -14
View File
@@ -649,21 +649,32 @@ impl AppServer {
let displayed_address = devserver.displayed_address();
// Always open the server first after the client has been built
// Only open the server if it isn't prerendered
// Only open the server if it isn't prerendered and finished building
if let Some(server) = self.server.as_mut().filter(|_| !self.ssg) {
tracing::debug!("Opening server build");
server.soft_kill().await;
server
.open(
devserver_ip,
displayed_address,
fullstack_address,
false,
false,
BuildId::SECONDARY,
&self.server_args,
)
.await?;
if server.stage < BuildStage::Success {
tracing::trace!("Skipping server open: will open once build completes");
} else {
tracing::debug!("Opening server build");
server.soft_kill().await;
server
.open(
devserver_ip,
displayed_address,
fullstack_address,
false,
false,
BuildId::SECONDARY,
&self.server_args,
)
.await?;
}
}
// Skip opening native client if still building (web can open anytime)
if self.client.build.bundle != BundleFormat::Web && self.client.stage < BuildStage::Success
{
tracing::trace!("Skipping client open: will open once build completes");
return Ok(());
}
// Start the new app before we kill the old one to give it a little bit of time