[Rust] tokio_tungstenite giving 400 Bad Request with Kite WebSocket

kitedev1983
When trying to connect to Kite connect V3's websocket URL using tokio_tungstenite (0.28.0), I get a 400 Bad Request. The same code with any other WebSocket URL (say, echo.websocket.org) connects just fine. Moreover, running socat with the exact same URL connects successfully. Code snippet attached below.
let url = "wss://ws.kite.trade?api_key=<API_KEY>&access_token=<ACCESS_TOKEN>";

println!("Connecting to {}...", url);
let (ws_stream, _) = tokio_tungstenite::connect_async(url).await?;
println!("Connected!");

let (mut write, mut read) = ws_stream.split();

let read_task = tokio::spawn(async move {
while let Some(msg) = read.next().await {
match msg {
Ok(msg) => {
println!("Received: {}", msg);
}
Err(e) => {
eprintln!("Error receiving message: {}", e);
break;
}
}
}
});
  • kitedev1983
    update: I've created this simple repo, which just attempts to create a connection to Kite's WebSocket endpoint - https://github.com/ayush-sb/ws_test

    Rust code output -
    $ cargo r -- --api-key <API_KEY> --access-token <ACCESS_TOKEN>
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
    Running `target/debug/ws_test --api-key <API_KEY> --access-token <ACCESS_TOKEN>`
    Connecting to wss://ws.kite.trade?api_key=<API_KEY>&access_token=<ACCESS_TOKEN>...
    Error: Http(Response { status: 400, version: HTTP/1.1, headers: {"server": "awselb/2.0", "date": "Wed, 18 Feb 2026 17:03:53 GMT", "content-type": "text/html", "content-length": "122", "connection": "close"}, body: Some([60, 104, 116, 109, 108, 62, 13, 10, 60, 104, 101, 97, 100, 62, 60, 116, 105, 116, 108, 101, 62, 52, 48, 48, 32, 66, 97, 100, 32, 82, 101, 113, 117, 101, 115, 116, 60, 47, 116, 105, 116, 108, 101, 62, 60, 47, 104, 101, 97, 100, 62, 13, 10, 60, 98, 111, 100, 121, 62, 13, 10, 60, 99, 101, 110, 116, 101, 114, 62, 60, 104, 49, 62, 52, 48, 48, 32, 66, 97, 100, 32, 82, 101, 113, 117, 101, 115, 116, 60, 47, 104, 49, 62, 60, 47, 99, 101, 110, 116, 101, 114, 62, 13, 10, 60, 47, 98, 111, 100, 121, 62, 13, 10, 60, 47, 104, 116, 109, 108, 62, 13, 10]) })
    Websocat output -
    $ websocat "wss://ws.kite.trade?api_key=<API_KEY>&access_token=<ACCESS_TOKEN>"
    {"type": "instruments_meta", "data": {"count": 171766, "etag": "W/\"69950bc7-1162\""}}
    {"type":"app_code","timestamp":"2026-02-18T22:28:35+05:30"}
  • Nivas
    Could you please verify whether the WebSocket handshake headers are being sent correctly, especially if you are using a lower-level WebSocket client that performs a minimal handshake? In some cases, differences in client handshake behavior can lead to a 400 Bad Request before the connection is established.

    For a more seamless experience and to avoid client-specific handshake nuances, we generally recommend using one of our official client libraries, which are designed to handle the WebSocket connection and protocol requirements appropriately.
  • kitedev1983
    Thanks for the reply, @Nivas

    I logged the headers being sent by the Rust client, they seem to match the ones being sent by Postman (which is able to successfully connect). Do you know of any other possible causes?

    Rust client headers -
    $ cargo r -- --api-key <API_KEY> --access-token <ACCESS_TOKEN>
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
    Running `target/debug/ws_test --api-key <API_KEY> --access-token <ACCESS_TOKEN>`
    Connecting to wss://ws.kite.trade?api_key=<API_KEY>&access_token=<ACCESS_TOKEN>...
    Outgoing request headers:
    host: ws.kite.trade
    connection: Upgrade
    upgrade: websocket
    sec-websocket-version: 13
    sec-websocket-key: ZQ+9GUmebLkca9uSTUIFXw==
    origin: https://kite.trade
    user-agent: tokio-tungstenite

    Output:
    Error: Http(Response { status: 400, version: HTTP/1.1, headers: {"server": "awselb/2.0", "date": "Fri, 20 Feb 2026 18:16:21 GMT", "content-type": "text/html", "content-length": "122", "connection": "close"}, body: Some([60, 104, 116, 109, 108, 62, 13, 10, 60, 104, 101, 97, 100, 62, 60, 116, 105, 116, 108, 101, 62, 52, 48, 48, 32, 66, 97, 100, 32, 82, 101, 113, 117, 101, 115, 116, 60, 47, 116, 105, 116, 108, 101, 62, 60, 47, 104, 101, 97, 100, 62, 13, 10, 60, 98, 111, 100, 121, 62, 13, 10, 60, 99, 101, 110, 116, 101, 114, 62, 60, 104, 49, 62, 52, 48, 48, 32, 66, 97, 100, 32, 82, 101, 113, 117, 101, 115, 116, 60, 47, 104, 49, 62, 60, 47, 99, 101, 110, 116, 101, 114, 62, 13, 10, 60, 47, 98, 111, 100, 121, 62, 13, 10, 60, 47, 104, 116, 109, 108, 62, 13, 10]) })
    Headers from Postman after successful connection -
    Connected to wss://ws.kite.trade?api_key=<API_KEY>&access_token=<ACCESS_TOKEN>

    Handshake Details
    Request URL: https://ws.kite.trade/?api_key=<API_KEY>&access_token=<ACCESS_TOKEN>
    Request Method: GET
    Status Code: 101 Switching Protocols

    Request Headers
    Sec-WebSocket-Version: 13
    Sec-WebSocket-Key: vAvdU0k+A5aEC2fx8ZFWYw==
    Connection: Upgrade
    Upgrade: websocket
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    Host: ws.kite.trade
    P.S. I'm aware of the official client libraries, but my current usecase requires a Rust-based client :smiley:
  • Nivas
    Since the request is receiving a 400 response from the AWS (awselb/2.0) rather than a WebSocket protocol or authentication error, the connection is likely being rejected during the initial handshake phase before reaching the Kite WebSocket service.

    Even if the visible headers appear similar, differences in client handshake characteristics such as TLS negotiation, extension headers (e.g., Sec-WebSocket-Extensions), request formatting, or underlying WebSocket stack behavior can lead to different outcomes across clients like Postman, websocat, and low-level libraries.

    Given that the same URL and credentials work with other WebSocket clients, this does not indicate a Kite Connect credential or endpoint issue, but rather a client-specific handshake or transport-level difference.
  • kiteuser_rs
    I was facing this issue too and went down a rabbit hole to finally figure it out ;)

    Turns out, the path component of an http url is required to contain a "/" as per the RFC:
    If the abs_path is not present in the URL, it MUST be given as "/" when used as a Request-URI for a resource
    This is a long-standing issue in the http crate used internally by tungstenite: https://github.com/hyperium/http/issues/176 (also see this reddit thread).

    Also this to say that your URL should include a "/" after the hostname before the query params:
    let url = "wss://ws.kite.trade/?api_key=<API_KEY>&access_token=<ACCESS_TOKEN>";

    @Nivas might be a good idea to update the websocket docs to include the "/" too ;)
  • kitedev1983
    @kiteuser_rs bingo, this was the exact problem! After Nivas pointed out that the 400 is from the ALB, I put this project aside for a while. It hit me around 10 days later, that there must be some really silly issue with the path. Hopefully this can be standardized soon somehow, would save a lot of time and pain
Sign In or Register to comment.