3

I am working on a Node.js proxy server that routes requests from a website to a localhost endpoint. The website can run either locally or on a cloud instance, so the proxy needs to support both HTTP and HTTPS requests and route them to the HTTP endpoint.

The code below works for almost all use cases, except when Websocket requests originate from an HTTPS address. Here is the code:

import * as express from "express";
import { Server, createServer } from "http";
import { createProxyServer } from "http-proxy";
import { settings } from "./settings"

export let port = 3004;

let server: Server;

export function startServer() {
    const URL = settings.URL;
    const app = express();
    server = createServer(app);

    const proxy = createProxyServer({
        target: URL,
        changeOrigin: !URL.includes("localhost"),
        ws: true,
        headers: {
            // Attach request headers
        },
    });

    server.on("upgrade", function (req, socket, head) {
        proxy.ws(req, socket, head, {});
    });

    app.get("/public/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.get("/api/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.post("/api/query", function (req, res) {
        proxy.web(req, res, {});
    });

    server.listen(0, () => {
        port = server?.address()?.port;
        console.log("Server started");
    });
}

export function stopServer() {
    if (server) {
        server.close();
    }
}

The line changeOrigin: !URL.includes("localhost") sets changeOrigin based on the host. It's unnecessary for localhost requests, but required for HTTPS requests.

This code, however, fails for the Websocket requests and returns the following error:

WebSocket connection to 'ws://localhost:61958/api/ws' failed:
//...

ERR read ECONNRESET: Error: read ECONNRESET
   at TCP.onStreamRead (node:internal/stream_base_commons:217:20)
   at TCP.callbackTrampoline (node:internal/async_hooks:130:17)

If I do not set changeOrigin for the Websocket endpoint via:

 server.on("upgrade", function (req, socket, head) {
    proxy.ws(req, socket, head, {changeOrigin: false});
  });

I get a different kind of error:

ERR Client network socket disconnected before secure TLS connection was established:
Error: Client network socket disconnected before secure TLS connection was established
  at connResetException (node:internal/errors:704:14)
  at TLSSocket.onConnectEnd (node:_tls_wrap:1590:19)
  at TLSSocket.emit (node:events:525:35)
  at endReadableNT (node:internal/streams/readable:1358:12)
  at process.processTicksAndRejections (node:internal/process/task_queues:83:21)

Any ideas on how to fix this? I feel like I'm overlooking something simple, but can't figure it out.

3
  • 1
    HTTPS? So you would need to detect the protocol of the incoming request and, for HTTPS, use createSecureServer, not createServer. And use a private key and (self-signed) certificate. Commented Jul 20, 2023 at 22:48
  • Do you know ngrok.com ? I feel like you are trying to create something similar... Commented Jul 24, 2023 at 17:15
  • @marioruiz, I haven't heard of ngrok, looks interesting but I have a very specific use case for a product I'm working on. Plus I don't see anything about Websockets in ngrok's docs. Commented Jul 25, 2023 at 6:32

1 Answer 1

1

I had a similar issue, where the proxy worked for https but not websockets. In my case the proxy runs as middleware within an authentication express app. The proxy forwards all the incoming traffic to localhost:8501 if the authentication was successful.

By stumbling over this post, I found a solution for my problem which I'll leave here in case it helps anyone else with a similar problem.

The following code forwards incoming http and websocket requests to a service running on localhost:8501.

It is very important to set changeOrigin: false for the websocket proxy because if the origin is changed, the response of the websocket is sent to the proxy and not the original sender of the websocket. This leads the a timeout on the websocket request. By setting it to false, the response is directly sent to the original sender of the websocket, bypassing the proxy for the response while the request is still being forwarded by the proxy.

const express = require('express')
const { createProxyMiddleware } = require('http-proxy-middleware')

const app = express()
const proxy = createProxyMiddleware({
    target: 'http://localhost:8501',
    changeOrigin: true,
    secure: false,
    ws: false,
});

const wsProxy = createProxyMiddleware({
    target: 'ws://localhost:8501',
    changeOrigin: false,
    secure: false,
    ws: true,
});

app.use('/', middleware.auth, proxy);

const server = app.listen(PORT, () => logger.default(`Server listening on port: ${PORT}`));
server.on("upgrade", wsProxy.upgrade)

In this solution, I'm using two proxies, one for http and one for websockets to make the configuration easier.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.