Friday, September 22, 2017

ASGI alongside mod_wsgi behind an Nginx proxy

Hi all,

I'm trying use Channels to add websockets functionality to an existing Django project.

At this stage, my websockets functionality is a simple echo of whatever text is sent by the client.

The  existing project runs on http://domain.com/, served by Apache+mod_wsgi behind a Nginx front-end proxy.

I'm trying to route websockets to http://domain.com/ws/, served by daphne behind the same Nginx front-end proxy.

The Nginx config looks like this:

  # daphne
  location /ws/ {
      proxy_pass  http://localhost:22222;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
  }

  # apache+mod_wsgi
  location / {
      proxy_pass  http://localhost:11111;
  }

When I do this, I can connect to the websocket on http://domain.com/ws/, but nothing gets echoed back.

Here's what I see from daphne in the console:

  2017-09-22 16:52:44,654 DEBUG    WebSocket daphne.response.npTbOappnj!BnXbgObcDS open and established
  127.0.0.1:43270 - - [22/Sep/2017:16:52:44] "WSCONNECT /" - -
  2017-09-22 16:52:44,654 DEBUG    WebSocket daphne.response.npTbOappnj!BnXbgObcDS accepted by application
  2017-09-22 16:52:52,569 DEBUG    WebSocket incoming frame on daphne.response.npTbOappnj!BnXbgObcDS
  2017-09-22 16:52:54,539 DEBUG    WebSocket incoming frame on daphne.response.npTbOappnj!BnXbgObcDS
  2017-09-22 16:52:55,229 DEBUG    WebSocket incoming frame on daphne.response.npTbOappnj!BnXbgObcDS
  2017-09-22 16:52:55,703 DEBUG    WebSocket incoming frame on daphne.response.npTbOappnj!BnXbgObcDS

The "WebSocket incoming frame on..." entries were logged each time my WS client sent a message.

I thought "WSCONNECT /" looked suspicious, since I'm trying to connect to "/ws", so I tried connecting to http://domain.com/ws/ws/ instead.

Sure enough, the echo worked when connected to http://domain.com/ws/ws/, and requests were logged as /ws.

So I think the problem seems to be that daphne isn't aware that it's running on a sub-path /ws.

I tried using the root-path parameter documented here https://github.com/django/daphne#root-path-script_name but that had no effect. The connections were still logged as / instead of /ws, and nothing gets echoed back.

As a test, I reconfigured Nginx to serve the whole site from daphne, eg:

  location / {
      proxy_pass  http://localhost:22222;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
  }

The app worked perfectly in that configuration - but I really need this to work alongside mod_wsgi.

The docs at https://channels.readthedocs.io/en/stable/deploying.html#running-asgi-alongside-wsgi specifically state that this is possible:

To do this, just set up your Daphne to serve as we discussed above, and then configure your load-balancer or front HTTP server process to dispatch requests to the correct server - based on either path, domain, or if you can, the Upgrade header. 
 
Dispatching based on path or domain means you'll need to design your WebSocket URLs carefully so you can always tell how to route them at the load-balancer level; the ideal thing is to be able to look for the Upgrade: WebSocket header and distinguish connections by this, but not all software supports this and it doesn't help route long-poll HTTP connections at all.

I suspect what's tripping me up is the "dispatching based on path or domain means you'll need to design your WebSocket URLs carefully" bit, but so far I can't see what I'm doing wrong.

My project is structured like this:

  myproject
  ├── db.sqlite3
  ├── manage.py
  ├── myapp
  │   ├── admin.py
  │   ├── apps.py
  │   ├── consumers.py
  │   ├── __init__.py
  │   ├── migrations
  │   │   ├── __init__.py
  │   ├── models.py
  │   ├── routing.py
  │   ├── tests.py
  │   └── views.py
  └── myproject
      ├── asgi.py
      ├── __init__.py
      ├── routing.py
      ├── settings.py
      ├── urls.py
      └── wsgi.py

myproject is a default Django project, and myapp is a default Django app, both with the minimum extra bits required for Channels.

myproject/asgi.py is:

  import os
  from channels.asgi import get_channel_layer
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
  channel_layer = get_channel_layer()


myproject/settings.py CHANNEL_LAYERS has this:

  "ROUTING": "myproject.routing.channel_routing",


myproject/routing.py has this:

  channel_routing = [
      # Include sub-routing from an app.
      include("myapp.routing.channel_routing", path=r"^/ws"),
  ]


myapp/routing.py has this:

  channel_routing = [
      route("websocket.receive", ws_message),
  ]

myapp/consumers.py:

  def ws_message(message):
      # ASGI WebSocket packet-received and send-packet message types
      # both have a "text" key for their textual data.
      message.reply_channel.send({
          "text": 'You said %s' % (message.content['text'],),
      })


Like I said, this all works great when I serve the entire project from Daphne, but I want mod_wsgi to serve HTTP stuff, as the docs suggest is possible.

What am I doing wrong here?

Thanks for reading this far,

Sean

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-users+unsubscribe@googlegroups.com.
To post to this group, send email to django-users@googlegroups.com.
Visit this group at https://groups.google.com/group/django-users.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/53072bc0-9d1c-4d0d-b1ee-c4b043630660%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

No comments:

Post a Comment