bliako has asked for the wisdom of the Perl Monks concerning the following question:

Estimado Monjees

I am looking for guidance on how to connect to a server via secure websocket (wss) using perl, at as high level as possible, using a module preferably. My browser initiates this connection with this header Sec-WebSocket-Key: which is base64-encoded binary data. My interaction with it would be mostly reading what it periodically sends.

I have looked AnyEvent::WebSocket::Connection and Net::Async::WebSocket::Client but they do not mention wss. Am I wrong?

Whereas Mojo::UserAgent does support wss (it says), and allows for headers to be passed on but alas it fails. I used something like this:

use strict; use warnings; use Mojo::UserAgent; my $URI = 'wss://echo.websocket.org'; my $ua = Mojo::UserAgent->new; my $tx = $ua->build_websocket_tx($URI); $tx->req->headers->user_agent('Mozilla/5.0 (X11; Linux x86_64) AppleWe +bKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'); print $tx->req->headers->to_string."\n"; $ua->start( $tx => sub { my ($ua, $tx) = @_; print "Headers: ".$tx->req->headers->to_string."\n"; print "WebSocket handshake failed!\n" and return unless $tx->is_websocket ; print "Got a handshake\n"; $tx->on(message => sub { my ($ua, $msg) = @_; print "WebSocket message: $msg\n"; $tx->finish; }); } ); print "waiting ...\n";

outputs this:

waiting ... UA: Mojo::UserAgent TX: Mojo::Transaction::HTTP WebSocket handshake failed!

I am using a test site for the target (https://ws-playground.netlify.app/) which works on the browser and I have checked it does not need extra headers/cookies. But above code fails.

10min Edit: this works (nodejs client):

wscat -c wss://echo.websocket.org -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'

this does not (waits...) :

curl -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36' 'wss://echo.websocket.org'

Replies are listed 'Best First'.
Re: Connect to a *secure* websocket server (wss)
by Corion (Patriarch) on Sep 24, 2025 at 14:19 UTC

    I've encountered that error at least twice and was twice stumped by it.

    You're launching an asynchronous connection, but never launch the IO loop driving everything. Add this at the bottom of your script:

    Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
Re: Connect to a *secure* websocket server (wss)
by cavac (Prior) on Sep 24, 2025 at 14:33 UTC

    It really depends on your needs. I'm mostly used to the server side of things writing cyclic executives, so i'm not sure what event based options for clients are available ;-)

    The couple of times i needed a websocket client, i did it "by hand" using Protocol::WebSocket::Frame after connecting via IO::Socket::SSL, because that works well enough for my own cyclic needs.

    Connection is easy, unless the W3C has re-defined things yet again. Here's an untested(!) minimal cyclic executive (a "worker") based on some of my existing codebase. I didn't include all that much error handling here, though, to keep the code small.

    use IO::Socket::SSL; use Protocol::WebSocket::Frame; use Carp; use Time::HiRes qw(sleep); use Data::Dumper; # Connect to server my $sock = IO::Socket::SSL->new( PeerHost => $self->{server}, PeerPort => $self->{port}, SSL_verify_mode => SSL_VERIFY_NONE, ) or croak($!); binmode($sock); # Switch to unblocking after the SSL initiation $sock->blocking(0); # Write the header my $header = "GET /public/api/blablabla HTTP/1.1\r\n" . "Host: example.com\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" . "Sec-WebSocket-Protocol: chat\r\n" . "Sec-WebSocket-Version: 13\r\n" . "Origin: https://cavac.at\r\n" . "\r\n"; $syswrite($sock, $header); # "Parse" the return headers from the server by just skipping over it, + hoping for the best ;-) syswrite($sock, $header); my $line = ""; while(1) { my $char; sysread($sock, $char, 1); if(defined($char) && length($char)) { if($char eq "\r") { next; } elsif($char eq "\n") { if($line eq "") { # end of header last; } else { $line = ""; } } else { $line .= $char; } } else { sleep(0.05); # wait a few microseconds for more data } } # Initiate the websocket frame handler my $frame = Protocol::WebSocket::Frame->new(max_payload_size => 500 * +1024 * 1024, masked => 1); # Send a frame { my %request = ( 'request-type' => 'HelloWorld', 'message-id' => 'rand' . int(rand(1_000_000)), 'other-stuff' => 'Hello Welt!', ); my $outframe = $frame->new(buffer => encode_json(\%request), type +=> 'text', masked => 1)->to_bytes; syswrite($sock, $outframe); } # Handle messages from server while(1) { # Read from socket and append data to the frame parser my $workCount = 0; while(1) { my $char; sysread($sock, $data, 1_000); last if(!defined($data) || !length($data)); $frame->append($data); $workCount++; } # Parse and react to messages from server while(my $data = $frame->next_bytes) { my $msg = decode_json($data); print Dumper($msg); # Just print it out } if(1) { # Other cyclic work you need to do in the program # doSomeStuff(); # $workCount++; } if(!$workCount) { # Nothing happened, just sleep a bit, try again later. Save on + CPU cycles sleep(0.05); } }

    Hope that helps.

    PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP
    Also check out my sisters artwork and my weekly webcomics

      cavac thank you for this code. I think I will continue with a high-level module though since Corion solved my problem above, i will continue with that. thank you.