{"record":{"id":"b4878a52f30bd00e","repo":"windmill-labs/windmill","slug":"http-proxy-sent-unexpected-bytes-after-connect-res","errorCode":null,"errorMessage":"HTTP proxy sent unexpected bytes after CONNECT response","messagePattern":"HTTP proxy sent unexpected bytes after CONNECT response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/windmill-trigger-websocket/src/proxy.rs","lineNumber":308,"sourceCode":"    }\n\n    if !status_ok {\n        return Err(io::Error::new(\n            io::ErrorKind::Other,\n            format!(\n                \"HTTP proxy CONNECT to {} rejected: {}\",\n                host_header,\n                status_line.trim_end()\n            ),\n        ));\n    }\n\n    // A conforming proxy stays silent after the CONNECT response until the\n    // client speaks. If our read buffer is non-empty, the proxy spoke\n    // first — handing the raw socket to TLS would silently drop those\n    // bytes and break the handshake.\n    if !reader.buffer().is_empty() {\n        return Err(io::Error::new(\n            io::ErrorKind::Other,\n            \"HTTP proxy sent unexpected bytes after CONNECT response\",\n        ));\n    }\n\n    Ok(reader.into_inner())\n}\n\n#[cfg(test)]\nmod tests {\n    //! The single live test (`http_connect_tunnel_…_unwraps_stream`) drives\n    //! a real `TcpListener` masquerading as a proxy and verifies both the\n    //! on-the-wire CONNECT request and that the returned `TcpStream`\n    //! actually carries tunneled bytes. The other proxy-URL and NO_PROXY\n    //! checks are kept under `#[ignore]` for manual debugging — they cover\n    //! logic that's mostly delegated to `url::Url::parse` and trivial\n    //! string matching, so re-running them on every CI build is low ROI.\n    use super::*;","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-trigger-websocket/src/proxy.rs#L290-L326","documentation":"After reading the CONNECT response, `http_connect_tunnel` checks `BufReader`'s internal buffer. A conforming HTTP proxy sends nothing after the CONNECT response until the client speaks. If buffered bytes remain, the proxy already sent extra data (pipelined response, injected banner, HTTP redirect, or a non-CONNECT response body); handing the raw socket to the TLS layer would silently discard those bytes and corrupt the WebSocket/TLS handshake, so the function fails fast.","triggerScenarios":"Calling `connect_async_with_proxy` (via `test_connection` or `get_consumer`) when the proxy sends bytes immediately after its CONNECT response — e.g. it sent a redirect body, an error page after a non-standard status, TLS on the proxy link, or is not actually an HTTP CONNECT proxy (some intercepting/mTLS proxies greet the client first).","commonSituations":"Pointing the client at a TLS/HTTPS proxy port with a plaintext CONNECT; pointing at a SOCKS proxy or transparent interception proxy that isn't CONNECT-capable; an anti-virus / SSL-inspection appliance injecting a banner; a misbehaving proxy that pipelines responses.","solutions":["Verify the proxy URL scheme matches the proxy listener (plain http:// CONNECT port vs https:// TLS port) and fix the scheme","Confirm the endpoint is an HTTP CONNECT proxy, not a SOCKS proxy (use a SOCKS-aware config or change the proxy)","Capture traffic (tcpdump/Wireshark on the client↔proxy link) to see what bytes the proxy sends after the response","Disable or reconfigure any TLS-interception appliance (e.g. MITM AV/firewall module) for this destination"],"exampleFix":"// before\nlet proxy = \"http://proxy.corp:443\"; // 443 is the TLS listener -> banner bytes after CONNECT\n// after\nlet proxy = \"https://proxy.corp:443\"; // or \"http://proxy.corp:3128\" for the plain listener","handlingStrategy":"validation","validationCode":"// fail fast if the endpoint is not a plain HTTP CONNECT proxy\nlet parsed = url::Url::parse(&proxy_url)?;\nlet is_plain_http = parsed.scheme() == \"http\";\nlet is_socks = matches!(parsed.scheme(), \"socks4\" | \"socks4a\" | \"socks5\");\nif !is_plain_http && !is_socks {\n    return Err(anyhow!(\"unsupported/ambiguous proxy scheme: {}\", parsed.scheme()));\n}","typeGuard":null,"tryCatchPattern":"match connect_async_with_proxy(&url, &proxy).await {\n    Err(WsError::Io(e)) if e.to_string().contains(\"unexpected bytes after CONNECT\") => {\n        eprintln!(\"{} is not a conforming CONNECT proxy: {}\", proxy, e);\n        // fix scheme/port or replace the intercepting proxy before retrying\n    }\n    Err(e) => return Err(e.into()),\n    Ok(_) => {}\n}","preventionTips":["Match the proxy URL scheme to the listener: never point an http:// CONNECT client at a TLS port (or vice versa)","Confirm the endpoint is HTTP CONNECT, not SOCKS — SOCKS proxies need a SOCKS client, not CONNECT tunneling","Exclude this destination from TLS-interception/MITM appliances in the network path","Add a preflight CONNECT check in CI/deployment to detect non-conforming proxies early"],"tags":["network","proxy","tls","http-connect","protocol-violation"],"backgroundTag":"proxy-protocol-violation","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}