From 77a50f916f4c441bc99850e819f1e73fd375c5c0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 13 Mar 2026 13:01:42 -0500 Subject: [PATCH] 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 --- packages/cli/src/serve/proxy.rs | 103 ++++++++++++++++++++----------- packages/cli/src/serve/runner.rs | 39 +++++++----- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/packages/cli/src/serve/proxy.rs b/packages/cli/src/serve/proxy.rs index 0fdcf3ee4..2e6f620cf 100644 --- a/packages/cli/src/serve/proxy.rs +++ b/packages/cli/src/serve/proxy.rs @@ -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, 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, + handle_error: fn(Error) -> Response, +) -> std::result::Result, Response> { + 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) -> Result> { - 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, ) -> MethodRouter { - let client = ProxyClient::new(url.clone()); - any(move |parts: Parts, mut req: Request| 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) => { diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index c405823a0..2810e7d4f 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -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