WebSocket Connection Fails with 301 Moved Permanently on DigitalOcean App Platform with NGINX

  Kiến thức lập trình

I have a React frontend and a .NET Core 8 backend running on DigitalOcean App Platform. The backend acts as a WebSocket server. Everything works when tested locally or with tools like Postman, but my frontend fails to connect to the WebSocket server when deployed.

The WebSocket connection attempt results in a 301 Moved Permanently error:

GET wss://example.com/ws
Status: 301 Moved Permanently

Backend Configuration:

My .NET Core WebSocket server is configured as follows:

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws" || context.Request.Path == "/game")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
            string connId = Guid.NewGuid().ToString();
            var webSocketManager = app.Services.GetRequiredService<WebSocketMan>();
            webSocketManager.AddSocket(connId, webSocket);
            await webSocketManager.HandleWebSocketCommunication(webSocket, connId, context);
        }
        else
        {
            context.Response.StatusCode = 400;
        }
    }
    else
    {
        await next();
    }
});

CORS Policy:

var allowedOrigins = Environment.GetEnvironmentVariable("ALLOWED_ORIGINS")?.Split(',') ?? new string[] { "http://localhost:3000", "https://example.com" };
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSpecificOrigin", policy =>
        policy.WithOrigins(allowedOrigins)
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials());
});

Frontend Configuration:

My frontend WebSocket code uses the following environment variables:

.env.production:

REACT_APP_API_URL=https://example.com/api
REACT_APP_WS_URL_ws=wss://example.com/ws
REACT_APP_WS_URL_game=wss://example.com/game

WebSocket Initialization example:

const ws = new WebSocket(process.env.REACT_APP_WS_URL_ws);

ws.onopen = () => {
    console.log('WebSocket connection opened');
};

ws.onmessage = (event) => {
    console.log('Message from server:', event.data);
};

ws.onclose = () => {
    console.log('WebSocket connection closed');
};

ws.onerror = (error) => {
    console.error('WebSocket error:', error);
};

NGINX Configuration:

The NGINX configuration in my frontend Dockerfile:

server {
    listen 80;
    server_name example.com;

    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://backend-api:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location /ws/ {
        proxy_pass http://backend-api:5000/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        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_cache_bypass $http_upgrade;
    }

    location /game/ {
        proxy_pass http://backend-api:5000/game;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        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_cache_bypass $http_upgrade;
    }
}

Dockerfile that I use on deployment for the front end:

FROM node:16-alpine AS build

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm install

COPY . .

RUN npm run build


FROM nginx:alpine

COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Using curl within the frontend container, the WebSocket handshake succeeds:

curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: example.com" -H "Origin: https://example.com" -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" -H "Sec-WebSocket-Version: 13" http://backend-api:5000/ws
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Date: Thu, 16 May 2024 16:48:30 GMT
Server: Kestrel
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com
Upgrade: websocket
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

I can also use tools like Postman and successfully connect to wss://example.com/ws but my frontend refuses to connect. Api calls work fine however.

This is my first time using the App Platform so I assume I’m doing something wrong on deployment. Any guidance appreciated.

LEAVE A COMMENT