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:
<code>GET wss://example.com/ws
Status: 301 Moved Permanently
<code>GET wss://example.com/ws
Status: 301 Moved Permanently
</code>
GET wss://example.com/ws
Status: 301 Moved Permanently
Backend Configuration:
My .NET Core WebSocket server is configured as follows:
<code>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);
context.Response.StatusCode = 400;
<code>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();
}
});
</code>
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:
<code>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)
<code>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());
});
</code>
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:
<code>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
<code>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
</code>
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:
<code>const ws = new WebSocket(process.env.REACT_APP_WS_URL_ws);
console.log('WebSocket connection opened');
ws.onmessage = (event) => {
console.log('Message from server:', event.data);
console.log('WebSocket connection closed');
ws.onerror = (error) => {
console.error('WebSocket error:', error);
<code>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);
};
</code>
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:
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
proxy_pass http://backend-api:5000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://backend-api:5000/ws;
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;
proxy_pass http://backend-api:5000/game;
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;
<code>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;
}
}
</code>
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:
<code>FROM node:16-alpine AS build
COPY package.json package-lock.json ./
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
<code>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;"]
</code>
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:
<code>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
Date: Thu, 16 May 2024 16:48:30 GMT
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
<code>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=
</code>
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.