I am trying to dynamically create/destroy "websocket-capable" paths in CherryPy, using ws4py. Here is a full program that demonstrates the problem:
import cherrypy
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
from ws4py.websocket import EchoWebSocket, WebSocket
class Nothing(object):
@cherrypy.expose
def index(self):
pass
class Root(object):
@cherrypy.expose
def index(self):
return "Tweep tweep!"
@cherrypy.expose
def add(self, bird):
# Try to create a new websocket-capable path.
cherrypy.tree.mount(Nothing(), "/bird/" + bird, config={"": {"tools.websocket.on": True, "tools.websocket.handler_cls": EchoWebSocket}})
@cherrypy.expose
def remove(self, bird):
# Remove a previously created websocket-capable path.
del cherrypy.tree.apps["/bird/" + bird]
@cherrypy.expose
def other(self):
pass
cherrypy.config.update({"server.socket_host": "localhost", "server.socket_port": 9000})
WebSocketPlugin(cherrypy.engine).subscribe()
cherrypy.tools.websocket = WebSocketTool()
cherrypy.quickstart(Root(), "/", config={"/other": {"tools.websocket.on": True,"tools.websocket.handler_cls": EchoWebSocket}})
This is as simple an example as I could construct: the Root class is placed as the main application, along with a ws4py config directive to allow for creation of websockets at ws://localhost:9000/other
. The add()
method creates a new application and mounts it at an appropriate path, to mimic the setup of the "/other" application.
After starting the server, I can do this in Chrome's JavaScript console:
> w = new WebSocket("ws://localhost:9000/other")
WebSocket {binaryType: "blob", extensions: "", protocol: "", onclose: null, onerror: null…}
> w.onmessage = function (d) { console.log(d.data); }
function (d) { console.log(d.data); }
> w.send("testing 1 2 3")
true
testing 1 2 3
Fantastic, it works!
Now, after visiting http://localhost:9000/add/eagle
in my browser (to cause the creation of a new path), I get the following exchange in the console:
> w = new WebSocket("ws://localhost:9000/bird/eagle")
WebSocket {binaryType: "blob", extensions: "", protocol: "", onclose: null, onerror: null…}
WebSocket connection to 'ws://localhost:9000/bird/eagle' failed: Unexpected response code: 301
Hmm... why do I get a 301? Just to show the difference between "/bird/eagle" and some other path that I didn't "create" using the "add" path:
> w = new WebSocket("ws://localhost:9000/bird/pelican")
WebSocket {binaryType: "blob", extensions: "", protocol: "", onclose: null, onerror: null…}
WebSocket connection to 'ws://localhost:9000/bird/pelican' failed: Unexpected response code: 404
The 404 makes sense; there is no such path on the server. But why do I get a 301 after mounting a new app specifically for this websocket creation purpose? Why does it behave differently from the one set up at server start time (on path "/other")? And what might I do differently to accomplish this behavior that I'm after?
Though I don't understand why I get that 301, I did figure out how to actually make this work the way I want it to. The trick seems to be that I can't install a websocket handler on the "index" path of a handler object. Instead, you need to do it on some other named path. My example program changed in just two places:
(1) The "Nothing" class gained a new method:
@cherrypy.expose
def ws(self):
pass
(2) The line that mounts the dynamically created handler changes its mount point:
cherrypy.tree.mount(Nothing(), "/bird/" + bird, config={"/ws": {"tools.websocket.on": True, "tools.websocket.handler_cls": EchoWebSocket}})
Now things work very nicely. I can add a bird at the "add/" URL, then instantiate a websocket object from the console and communicate through it, and then finally I can remove the bird at the "remove/" URL.