与 'wss://api-such.andsuch.xyz/graphql/' 的 WebSocket 连接失败:WebSocket 握手期间出错:意外的响应代码:400

WebSocket connection to 'wss://api-such.andsuch.xyz/graphql/' failed: Error during WebSocket handshake: Unexpected response code: 400

我最近将我正在从事的项目部署到生产环境中。我使用 DjangoChannelsGraphqlWs for GraphQL subscription functionalities. and I have GraphQL Playground set up via django-graphql-playground。在本地一切正常 - 没有任何问题 - 订阅工作正常。但是,当我部署时,在 Playground 中点击播放按钮时出现以下错误:

{
  "error": "Could not connect to websocket endpoint wss://api-such.andsuch.xyz/graphql/. Please check if the endpoint url is correct."
}

...在我的浏览器控制台中,它显示

WebSocket connection to 'wss://api-such.andsuch.xyz/graphql/' failed: Error during WebSocket handshake: Unexpected response code: 400

需要注意的一件事是该应用程序已 dockerized。可能是从那里来的吗?我不这么认为,因为它在本地有效。这是我的 docker-compose 文件的样子:

version: '3.7'

services:
  nginx:
    container_name: nginx
    image: nginx
    restart: always
    depends_on:
      - web
    volumes:
      - ./web/dev.nginx.template:/etc/nginx/conf.d/dev.nginx.template
      - ./static:/static
      - ./media:/media
    ports:
      - "8080:80"
    networks:
      - SOME_NETWORK
    command: /bin/bash -c "envsubst \"`env | awk -F = '{printf \" $$%s\", $}'`\" < /etc/nginx/conf.d/dev.nginx.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"

  web:
    container_name: web
    restart: always
    build: ./web
    networks:
      - SOME_NETWORK
    depends_on:
      - postgres
      - redis
    volumes:
      - ./web:/usr/src/app/
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - GRAPHQL_ENDPOINT=https://api-such.andsuch.xyz/graphql/
    command: bash -c /start.sh

  postgres:
    container_name: postgres
    restart: always
    image: postgres:latest
    networks:
      - SOME_NETWORK
    volumes:
      - pgdata:/var/lib/postgresql/data/

  redis:
   container_name: redis
   restart: always
   image: redis:latest
   networks:
    - SOME_NETWORK
   ports:
     - "6379:6379"
   volumes:
     - redisdata:/data

volumes:
  pgdata:
  redisdata:

networks:
  SOME_NETWORK:
    name: SOME_NETWORK
    driver: bridge

settings.py

...
...
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [(os.getenv('REDIS_HOST', 'redis'), os.getenv('REDIS_PORT', 6379))],
        }
    }
}
...
...

routing.py

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
        URLRouter([
            path('graphql/', GraphQLWSConsumer)
        ]),
    )
})

consumers.py

class GraphQLWSConsumer(channels_graphql_ws.GraphqlWsConsumer):
    schema = schema

    async def on_connect(self, payload):
        self.scope['user'] = await get_user(self.scope)

urls.py

...
...
from graphql_playground.views import GraphQLPlaygroundView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('playground/', GraphQLPlaygroundView.as_view(
        endpoint=os.getenv('GRAPHQL_ENDPOINT'))),
]
...

nginx

server {
    client_max_body_size 10M; 
    
    listen 443 ssl;
    listen [::]:443 ssl;
    
    server_name api-such.andsuch.xyz;
    
    ssl_certificate /etc/ssl/certs/andsuch.xyz.pem;
    ssl_certificate_key /etc/ssl/certs/andsuch.xyz.key;
    
    location = /favicon.ico { access_log off; log_not_found off; }
    
    location / {
        proxy_set_header Host $http_host;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection ‘upgrade’;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://0.0.0.0:8080;
    }
}

有什么问题吗?我没主意了。谢谢!

更新 我检查了 chrome 的开发人员控制台中的网络选项卡,发现 websocket 连接立即关闭。为什么会这样?

您缺少 routing.py 文件;例如:

from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter
from . import consumers


application = ProtocolTypeRouter({
    "websocket": URLRouter([
        path("api-such.andsuch.xyz/graphql/", consumers.MyGraphqlConsumer),
    ]),
})

虽然 urls.py 声明了所有已发布的 HTTP end-points,但对于其他协议(尤其是 websockets),您应该使用 routing.py

在负责处理所有 websocket 事件的消费者中,您应该立即 accept 传入连接,因为这是协议要求的:

from channels.consumer import SyncConsumer

class MyGraphqlConsumer(SyncConsumer):

    def websocket_connect(self, event):
        self.send({
            'type': 'websocket.accept'
        })

否则,连接将在短时间超时后失败。

阅读大量文章后,我发现 this one, and this section 让我意识到唯一的错误是我没有在内部向其他 docker 服务公开端口 8000。在我的 docker-compose 文件中,Web 服务应该有...

...
...
expose:
    - 8000
...
...

...在里面。我添加了它并解决了。