Solved: Mojolicious Websockets Inactivity timeout

in perl

A recent snippet of Mojolicious webapp I recently wrote would not allow browsers to connect and upgrade to websockets. Below you'll find the code and explanation so hopefully it'll save you some debugging time.

Bug Symptoms

Connecting to the websocket endpoint gave me the following error in the browser console:

WebSocket connection to 'ws://localhost:3000/feed/news' failed: Connection closed before receiving a handshake response
(anonymous) @ update:18

And at the same time in the server console:

[debug] Inactivity timeout

So it looks like the server was hanging up on the client instead of upgrading the connection to websockets. But why?

Controller Code

The answer's obvious when we look at the controller code:

sub news {
    my $self = shift;
    my $id = sprintf "%s", $self->tx;
    $clients->{$id} = $self->tx;
}

When a new client connects we ask Mojolicious to store its transaction in a dictionary, so we'll be able to send news to the new client. But we never add any subscribers to the new client's events.

Without subscribers Mojo doesn't know you want to keep the connection alive and simply hangs up when the function ends.

It's not even that bad because in any sitation where we store a transaction object we'll need to delete it when the connection closes. So the correct version is actually:

sub news {
    my $self = shift;
    my $id = sprintf "%s", $self->tx;
    $clients->{$id} = $self->tx;

    $self->on(finished => sub {
            delete $clients->{$id};
        });
}

The first implementation was buggy and leaked memory, because it kept tx objects alive in a dictionary even after the connection ended. The second version is how it should be written, clearing up the memory after clients disconnect.

Unsurprisingly this version also works as expected and allows clients to upgrade their connection to websocket.

Conclusions

To open a Websocket connection in Mojolicious we need 3 things:

  1. Create a websocket route:
$r->websocket('/feed/news')->to('feed#news');
  1. Add at least one subscriber in the controller listening to the socket:
$self->on(finished => sub {
    delete $clients->{$id};
});
  1. Create a WebSocket object from JavaScript connecting to the server:
const sock = new WebSocket("ws://localhost:3000/feed/news");
sock.onmessage = function(event) {
  var data = JSON.parse(event.data);
};

Comments