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
- Move the app's dev server into 3000-9999 (e.g. 3306 -> 5433-style change; 80 -> 3000) and update the microfrontends config
- Keep HMR/WS ports inside the range too
- 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
- Standardize dev-server ports to 3000-9999 in repo tooling
- Include HMR/WS ports in config review
- Treat blocklist hits as config bugs, never bypass them
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
- Directory path contains potentially unsafe characters: ${dir
- Invalid ${description}: path must be an absolute, non-empty
- daemon socket path is owned by another user: {path}
- owner-only security descriptor did not contain a DACL
- daemon socket path did not have an owner SID
AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17).
Data as JSON: /api/errors/677f723a651157e3.
Report an issue: GitHub.