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?
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.