Search code examples
rustwebsocketdjango-channelsrust-tokio

Rust Tokio to Django Channels 4 `sec-websocket-key` error


I've written a websocket client in Rust, using tokio_tungstenite::tungstenite::http::Request to establish a connection, and running a Django Channels 4 server on an ASGI/Daphne version 4.0.0 development server.

When I use a current version of tokio (specifically 0.20), I get the following error:

Protocol(InvalidHeader("sec-websocket-key"))

However, when I downgrade tokio to 0.14, the client code runs without issues and the websocket connection is established and functions as expected. (note that I haven't tried other versions inbetween - it just so happens that I had previously gotten it to work on 0.14, and just wanted to upgrade to the most recent version for this project, which happens to be 0.20)

After a bit of reading, I understand that the key is part of the handshake that establishes the websocket connection. However, it also appears that the tungstenite connector should be able to automatically generate this, and I shouldn't be expected to set this myself in my headers.

These lines are probably most relevant:

    let request = Request::builder()
        .uri(&uri)
        .header("Origin", origin)
        .body(()).unwrap();

    let res = connect_async(request).await;
    match res {
       Ok((ws_stream, _)) => {
       ...

Please disregard the unwrapping - this is not production code, I'm interested in solving the technical issues before making things nicer.

Should I in fact generate a key, and set it in the headers? Is there a way to get tungstenite to do that for me in concert while I still want to set an Origin header? Or am I missing the bigger picture here?

After reading How to set origin header to websocket client in Rust? I tried the following:

    let request = Request::builder()
        .uri(&uri)
        .header("Host", host)
        .header("Origin", origin)
        .header("Connection", "Upgrade")
        .header("Upgrade", "websocket")
        .header("Sec-WebSocket-Version", "13")
        .header("Sec-WebSocket-Key", tokio_tungstenite::tungstenite::handshake::client::generate_key())
        .body(()).unwrap();

But this results in another error which suggests that the server is not happy with the attempt:

Http(Response { status: 400, version: HTTP/1.1, headers: {}, body: Some([]) })

And if I downgrade back to 0.14, the code won't even work, as generate_key() is private in that version, so it seems that tungstenite doesn't expect me to call it myself (or at least didn't used to).

What may tokio 0.14 be doing that 0.20 does not, and how should I resolve this?


Solution

  • User @kmdreko looked more closely and found that the likely culprit was the "Host" header field, which it was. The field was being passed without the port number.

    However, although the following works, with a correct value for host:

    let request = Request::builder()
        .uri(&uri)
        .header("Host", host)
        .header("Origin", origin)
        .header("Connection", "Upgrade")
        .header("Upgrade", "websocket")
        .header("Sec-WebSocket-Version", "13")
        .header("Sec-WebSocket-Key", tokio_tungstenite::tungstenite::handshake::client::generate_key())
        .body(()).unwrap();
    

    This is a bit unsatisfying, since all these values are somewhat arbitrary and I wanted to leave it up to tungstenite to provide sensible defaults.

    This did not work:

    let request = Request::builder()
        .uri(&uri)
        .header("Origin", origin)
        .body(()).unwrap();
    

    However, this is the solution I was looking for:

    use tokio_tungstenite::tungstenite::http::HeaderValue;
    use tokio_tungstenite::tungstenite::client::IntoClientRequest;
    
    // ..
    
    let mut request = uri.into_client_request().unwrap();
    let origin: HeaderValue = origin.parse().unwrap();
    request.headers_mut()
        .insert("Origin", origin);
    

    By defining request as mutable and then inserting the needed "Origin" field on the Request generated by tungstenite, I was able to get a working solution where tungstenite sets all the other fields to their required default/current settings.