vercel/turborepo · error · std::io::Error

Port validation failed: {e}

Error message

Port validation failed: {e}

What it means

Before proxying a request to an app's dev server, handle_request (http.rs:51) runs validate_port from ports.rs: only ports 3000-9999 are allowed, and a blocklist (22, 23, 25, 110, 143, 443, 3306, 5432, 6379, 27017) is checked first even inside that range. The failure is wrapped as PermissionDenied 'Port validation failed: {e}' with the concrete reason, as an SSRF guard so the proxy can never be pointed at system services.

Source

Thrown at crates/turborepo-microfrontends-proxy/src/http.rs:51

    .await;

    handle_forward_result(result, path, route_match, remote_addr, http_client, "HTTP").await
}

pub(crate) async fn forward_request(
    mut req: Request<Incoming>,
    app_name: &str,
    port: u16,
    remote_addr: SocketAddr,
    http_client: HttpClient,
) -> Result<Response<Incoming>, Box<dyn std::error::Error + Send + Sync>> {
    // Validate port to prevent SSRF attacks
    validate_port(port).map_err(|e| {
        warn!(
            "Port validation failed for {} (port {}): {}",
            app_name, port, e
        );
        Box::new(std::io::Error::new(
            std::io::ErrorKind::PermissionDenied,
            format!("Port validation failed: {e}"),
        )) as Box<dyn std::error::Error + Send + Sync>
    })?;

    let target_uri = format!(
        "http://localhost:{}{}",
        port,
        req.uri()
            .path_and_query()
            .map(|pq| pq.as_str())
            .unwrap_or("/")
    );

    let original_host = validated_host_header(&req)?.to_string();

    let headers = req.headers_mut();
    headers.insert("Host", format!("localhost:{port}").parse()?);

View on GitHub (pinned to f9245100cf)

Solutions

  1. Move the app's dev server into 3000-9999 (e.g. 3306 -> 5433-style change; 80 -> 3000) and update the microfrontends config
  2. Keep HMR/WS ports inside the range too
  3. If you genuinely need another port, file a feature request — the range is a deliberate security policy, not a bug

Example fix

# before (turbo.json microfrontends proxy target)
"dev": { "port": 3306 }
# after
"dev": { "port": 5433 }
Defensive patterns

Strategy: validation

Validate before calling

// validate before assigning the proxy target port
use turborepo_microfrontends_proxy::ports::validate_port; // or replicate:
fn port_ok(p: u16) -> bool { (3000..=9999).contains(&p) && ![22,23,25,110,143,443,3306,5432,6379,27017].contains(&p) }
assert!(port_ok(app_port));

Type guard

fn is_allowed_dev_port(p: u16) -> bool {
    (3000..=9999).contains(&p) && !BLOCKED_PORTS.contains(&p)
}

Try / catch

// surfaced as Box<dyn Error> from handle_request — match on the PermissionDenied inner io::Error
if let Some(io) = err.downcast_ref::<std::io::Error>() {
    if io.kind() == std::io::ErrorKind::PermissionDenied && io.to_string().contains("Port validation") {
        return respond_502_with_config_hint(app_name); // tell user to fix the port
    }
}

Prevention

When it happens

Trigger: A microfrontends app configured with a dev-server port of 80/443/8080? (8080 is fine) — concretely: port < 3000 (e.g. 80, 300), port > 9999 (e.g. 10000, 30000 for HMR), or a blocked service port like 3306 (MySQL) or 6379 (Redis) that sits inside the allowed range.

Common situations: Apps started with --port 80 or 443 in containers HMR/websocket ports configured above 9999 Someone pointing the proxy at a local database 'just to see'

Related errors


AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17). Data as JSON: /api/errors/677f723a651157e3. Report an issue: GitHub.