Search code examples
dartdart-iodart-httpdart-shelf

How can I use shelf_web_socket to listen for http and ws requests on the same port


https://pub.dartlang.org/packages/shelf_web_socket shows this example

import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_web_socket/shelf_web_socket.dart';

void main() {
  var handler = webSocketHandler((webSocket) {
    webSocket.listen((message) {
      webSocket.add("echo $message");
    });
  });

  shelf_io.serve(handler, 'localhost', 8080).then((server) {
    print('Serving at ws://${server.address.host}:${server.port}');
  });
}

I would like to know how to combine this with my HTTP server initialization

import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as sIo;
import 'package:shelf_auth/shelf_auth.dart' as sAuth;
import 'package:shelf_auth/src/authentication.dart' as sAuth2;
import 'package:option/option.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart' as sWs;

...

var authMiddleware = sAuth.authenticate(
    [new MyAuthenticator()],
    sessionHandler: new sAuth.JwtSessionHandler('bla', 'blub', new UserLookup()),
    allowHttp: true,
    allowAnonymousAccess: false);

var handler = const shelf.Pipeline()
    .addMiddleware(shelf.logRequests())
    .addMiddleware(authMiddleware)
    .addHandler(_handleHttpRequest);

// var wsHandler = sWs.webSocketHandler(_handleWebSocketConnect);

sIo.serve(handler, '0.0.0.0', servePort).then((server) {
  _log.finest('Serving at http://${server.address.host}:${server.port}');
});

What needs to be done so that wsHandler gets called for WebSocket connects and handler keeps handling HTTP requests (if possible on the same port) and if possible uses the configured authentication and session management.

I tried it on a different port but with the authentication/session middleware (no idea if this is supposed to be used together)

   var authMiddleware = sAuth.authenticate(
        [new MyAuthenticator()],
        sessionHandler: new sAuth.JwtSessionHandler('bla', 'blub', new UserLookup()),
        allowHttp: true,
        allowAnonymousAccess: false);

    var handler = const shelf.Pipeline()
        .addMiddleware(shelf.logRequests())
        .addMiddleware(authMiddleware)
        .addHandler(_handleHttpRequest);

    sIo.serve(handler, '0.0.0.0', servePort).then((server) {
      _log.finest('Serving at http://${server.address.host}:${server.port}');
    });


    var wsHandler = const shelf.Pipeline()
      .addMiddleware(shelf.logRequests())
      .addMiddleware(authMiddleware)
      .addHandler(sWs.webSocketHandler(_handleWebSocketConnect));

    sIo.serve(wsHandler, '0.0.0.0', servePort + 1).then((server) {
      _log.finest('Serving at ws://${server.address.host}:${server.port}');
    });

and got

Illegal argument(s): webSocketHandler may only be used with a server that supports request hijacking.

Solution

  • At the moment your root handler is the http handler. You'll need to set up a handler that conditionally sends requests to the ws handler or another handler for your http requests. Eg

    /ws -> your ws handler

    /rest -> your other handler

    The easiest way to do that is to use a router like shelf_route.

    However someone recently tried this and hit a bug in shelf that stopped this working. Which as you noted below is fixed but not merged.

    Once the issue is fixed you should be able to do

    import 'package:shelf/shelf.dart' as shelf;
    import 'package:shelf/shelf_io.dart' as io;
    import 'package:shelf_route/shelf_route.dart' as route;
    import 'package:shelf_web_socket/shelf_web_socket.dart' as sWs;
    import 'package:shelf_auth/shelf_auth.dart' as sAuth;
    import 'dart:async';
    import 'package:option/option.dart';
    import 'package:shelf_exception_response/exception_response.dart';
    
    void main(List<String> arguments) {
    
      var authMiddleware = sAuth.authenticate(
            [new MyAuthenticator()],
            sessionHandler: new sAuth.JwtSessionHandler('bla', 'blub', new UserLookup()),
            allowHttp: true,
            allowAnonymousAccess: false);
    
    
      var router = (route.router()
          ..get('/rest', _handleHttpRequest)
          ..get('/ws', sWs.webSocketHandler(_handleWebSocketConnect)));
    
      var handler = const shelf.Pipeline()
          .addMiddleware(exceptionResponse())
          .addMiddleware(shelf.logRequests())
          .addMiddleware(authMiddleware)
          .addHandler(router.handler);
    
      route.printRoutes(router);
    
      io.serve(handler, '127.0.0.1', 8080).then((server) {
        print('Serving at http://${server.address.host}:${server.port}');
      });
    }
    

    Until the issue is fixed you can replace the router.handler with

    var hackyRouterHandler = (shelf.Request request) {
      var path = request.url.path;
      if (path.startsWith('/rest')) {
        return _handleHttpRequest(request);
      }
      else if (path.startsWith('/ws')) {
        return sWs.webSocketHandler(_handleWebSocketConnect)(request);
      }
      else {
        throw new NotFoundException();
      }
    };