{"record":{"id":"1733abe1ac012d08","repo":"shadowsocks/shadowsocks-rust","slug":"empty-outbound-proxy-chain","errorCode":null,"errorMessage":"empty outbound proxy chain","messagePattern":"empty outbound proxy chain","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/shadowsocks-service/src/net/outbound/chain.rs","lineNumber":45,"sourceCode":"        target: &Address,\n    ) -> io::Result<OutboundProxyStream>\n    where\n        D: TcpDialer + Sync,\n    {\n        connect_chain(self.hops(), dialer, target).await\n    }\n}\n\npub(crate) async fn connect_chain<D>(\n    hops: &[OutboundProxyHop],\n    dialer: &D,\n    target: &Address,\n) -> io::Result<OutboundProxyStream>\nwhere\n    D: TcpDialer + Sync,\n{\n    let Some(first_hop) = hops.first() else {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"empty outbound proxy chain\",\n        ));\n    };\n\n    trace!(\"dialling first outbound proxy hop {}\", first_hop.addr);\n    let first_tcp = dialer.dial(&first_hop.addr).await?;\n    let mut stream = OutboundProxyStream::from_tcp(first_tcp)?;\n\n    for (idx, hop) in hops.iter().enumerate() {\n        // For HTTPS hops, wrap the wire layer with TLS *before* speaking\n        // the application-level CONNECT verb.\n        if hop.is_https() {\n            stream = tls_wrap(stream, hop.tls_sni()).await?;\n        }\n\n        let next_target = hops\n            .get(idx + 1)","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/shadowsocks/shadowsocks-rust/blob/8eb0f0a65b1d976ab6bed5787327ef86529b0435/crates/shadowsocks-service/src/net/outbound/chain.rs#L27-L63","documentation":"connect_chain dials the first hop of an outbound proxy chain; it requires at least one configured hop. When the hops list is empty there is no proxy to dial, so the function returns InvalidInput before doing any I/O. This is a configuration error surfaced at connect time.","triggerScenarios":"Calling connect_tcp or connect_chain_for_udp_associate with an outbound config that resolved to a zero-length hop list (e.g. empty `outbound_forward` / proxy chain in config).","commonSituations":"Config file has an empty proxy chain array; the proxy section was commented out or omitted while chaining mode is still enabled; a loader filtered out all invalid proxy entries leaving none.","solutions":["Add at least one outbound proxy hop to the configuration before starting the server/client","Validate the hop list is non-empty at config-load time and fail fast with a clear message","If direct (non-proxied) outbound is intended, use the direct outbound path instead of connect_chain"],"exampleFix":"// before\nlet hops: Vec<OutboundProxyHop> = parse_hops(cfg); // empty\nconnect_chain(dialer, hops, target).await?;\n// after\nlet hops: Vec<OutboundProxyHop> = parse_hops(cfg);\nassert!(!hops.is_empty(), \"at least one outbound proxy hop required\");\nconnect_chain(dialer, hops, target).await?;","handlingStrategy":"validation","validationCode":"if outbound_hops.is_empty() {\n    return Err(anyhow!(\"outbound proxy chain must contain at least one hop\"));\n}","typeGuard":"fn has_hops(hops: &[OutboundProxyHop]) -> bool { !hops.is_empty() }","tryCatchPattern":"match connect_chain(dialer, hops, target).await {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput && e.to_string().contains(\"empty outbound proxy chain\") => configure_fallback_hop()?,\n    other => other?,\n}","preventionTips":["Validate the proxy chain is non-empty at config load time","Fail fast on startup rather than on first connection","Add a unit test for the empty-hops config case"],"tags":["proxy","configuration","validation"],"backgroundTag":"empty-required-field","analyzedSha":"8eb0f0a65b1d976ab6bed5787327ef86529b0435","analyzedAt":"2026-09-09T12:20:43.168Z","contentChangedAt":"2026-09-09T12:20:43.168Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}