WebSocket upgrade not reaching app: Metal Edge returns 426, TCP proxy returns 400
sparechangefinance
PROOP

24 days ago

We need WebSocket connections to reach our Node.js app on Railway. Right now both the main domain and the TCP proxy block the upgrade before it reaches our backend.

Setup

  • One service: Node.js HTTP server on the assigned PORT. It serves normal HTTP and WebSocket upgrades on the same port via server.on('upgrade').

  • WebSocket path: /ws/helius/:walletAddress (GET with Connection: Upgrade, Upgrade: websocket, Sec-WebSocket-Version: 13, Sec-WebSocket-Key).

  • When the upgrade reaches our app, we respond with 101 Switching Protocols and the WebSocket works. We’ve confirmed this in our logs when the request does reach us.

What we see

  1. HTTPS domain (Metal Edge)

wss://<our-service>.up.railway.app/ws/helius/...

  • Response: 426 Upgrade Required with Server: railway-edge.

  • The upgrade never hits our app; the edge is responding.

  1. TCP proxy

We added a TCP proxy in Networking (target port = our app’s PORT). Railway gave us a host and port (e.g. <something>.proxy.rlwy.net:<port>).

ws://<that-host>:<port>/ws/helius/...

  • Response: 400 Bad Request, Content-Type: text/html, body “Bad Request”.

  • Our app never sees this request. We only return 400 with a JSON body for invalid paths, so this 400 is from the TCP proxy, not our service.

What we need

A way for WebSocket upgrade requests to be forwarded to our deployment so our Node server can respond with 101 on the same port as HTTP.

  • Metal Edge: Can upgrade requests (GET with Connection: Upgrade, Upgrade: websocket, and the usual Sec-WebSocket-* headers) be forwarded to our service instead of returning 426?

  • TCP proxy: Is it expected to return 400 for HTTP upgrade requests? If so, is there a supported way to expose WebSockets so upgrades reach our app?

No secret keys or tokens are included in this report—only public domains and proxy host/port. Happy to provide our exact Railway domain and TCP proxy host/port in a private channel if that helps.

Thanks for any guidance.

Solved$10 Bounty

Pinned Solution

sparechangefinance
PROOP

19 days ago

The fix was removing the path option from new WebSocket.Server({ noServer: true, path: '/socket/helius' }). When using noServer: true, you handle path matching yourself in the upgrade handler, so the path option was interfering with the handshake.

2 Replies

Hey , I deployed a Node.js service that serves both HTTP and WebSocket on the same port using server.on('upgrade') with the ws library. WebSocket upgrades succeed over the Railway generated .up.railway.app domain.

Checklist

P1
Use wss:// (not ws://) with your Railway HTTPS domain
Make sure you have a public domain generated (PS: don't use the TCP proxy for WebSocket. )

The TCP proxy is for raw TCP, not HTTP upgrades ( Use the HTTPS domain instead. )

Handle the upgrade event on the HTTP server
P2
Your server must bind to 0.0.0.0 on the PORT env variable
Make sure nothing in your app is rejecting the upgrade before it reaches your WebSocket handler (middleware, auth checks, etc.)
Use wss://your-service - railway the HTTPS edge proxy forwards WebSocket upgrades correctly. Don't use the TCP proxy for this
Let me know if you face any issues !
Github https://github.com/dharmateja03/railway-bounty-testing
live link https://railway-bounty-testing-production.up.railway.app/


dharmateja

Hey , I deployed a Node.js service that serves both HTTP and WebSocket on the same port using server.on('upgrade') with the ws library. WebSocket upgrades succeed over the Railway generated .up.railway.app domain.ChecklistP1Use wss:// (not ws://) with your Railway HTTPS domainMake sure you have a public domain generated (PS: don't use the TCP proxy for WebSocket. )The TCP proxy is for raw TCP, not HTTP upgrades ( Use the HTTPS domain instead. )Handle the upgrade event on the HTTP serverP2Your server must bind to 0.0.0.0 on the PORT env variableMake sure nothing in your app is rejecting the upgrade before it reaches your WebSocket handler (middleware, auth checks, etc.)Use wss://your-service - railway the HTTPS edge proxy forwards WebSocket upgrades correctly. Don't use the TCP proxy for thisLet me know if you face any issues !Github https://github.com/dharmateja03/railway-bounty-testinglive link https://railway-bounty-testing-production.up.railway.app/

sparechangefinance
PROOP

19 days ago

The fix was removing the path option from new WebSocket.Server({ noServer: true, path: '/socket/helius' }). When using noServer: true, you handle path matching yourself in the upgrade handler, so the path option was interfering with the handshake.


Status changed to Solved brody 19 days ago


Loading...