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