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.
Ruslan Zhomir's answer alluded to a workaround: modifying the
originheader in the WebSocket upgrade request, using theproxyReqevent used inhttp-party/node-http-proxy.That may be suitable in scenarios where the
originheader is being used by the target endpoint to validate WebSocket connections, but this is not a common requirement. If you only need to handle simple, unsecured WebSocket connections, that might be enough.But, if you need to handle a mixture of secured and unsecured WebSocket connections, or if you are developing a production-grade application, you need a different approach.
Problem analysis
Right now, you are attempting to directly pass secure WebSocket upgrade requests from HTTPS clients to an HTTP target without properly managing and decrypting these secure requests. You need to add proper SSL/TLS handling to your proxy server to fix that.
In your original code, you are managing the
upgradeevent and using theproxy.wsmethod, similar to thenode-http-proxyREADME.When a client makes a WebSocket connection, it first sends a normal HTTP request and then requests to upgrade the connection to a WebSocket. In your case, if this initial request is sent over HTTPS, the request to upgrade the connection to a WebSocket is also sent over HTTPS.
However, in the code above, you are trying to directly pass this upgrade request to the proxy target. If the target is an HTTP endpoint, it will not be able to handle the encrypted request coming from an HTTPS client, resulting in the
ECONNRESETerror you are seeing.The current proxy settings also do not handle HTTPS WebSocket connections correctly:
Here, you are setting the
wsoption totrue, indicating that the proxy should be able to handle WebSocket requests. But since you are not providing any SSL/TLS options for handling HTTPS, the proxy is not able to correctly establish secure WebSocket connections. That is where the second error (about disconnecting before a secure TLS connection was established) comes from: you try to proxy WebSocket requests from HTTPS clients to an HTTP server.When a WebSocket request comes from an HTTPS client, the client expects to establish a secure WebSocket connection (
wss://). In order to properly accept and downgrade this secure connection to a non-secure one for the HTTP target, your proxy server needs to be set up to handle HTTPS. If it is not, it will not be able to correctly establish the secure WebSocket connection that the client is expecting.Your proxy server needs to correctly establish an HTTPS WebSocket connection first, then downgrade it to a non-secure WebSocket connection for the target.
Possible solution
Since your target is always an HTTP endpoint, and your server can receive either HTTP or HTTPS, you should handle the HTTPS connections separately and then pass it to the proxy.
That means creating an HTTPS server in addition to the HTTP one. Your HTTPS server should have all the necessary SSL/TLS configurations, and it can simply forward the requests to the HTTP server. A simple self-signed certificate can be created for this purpose.
Something like:
That code creates an HTTP server and an HTTPS server listening on different ports (3004/8443). The HTTPS server is using the
key.pemandcert.pemfor the SSL/TLS configuration. It handles the HTTPS WebSocket upgrade requests, while the HTTP server handles the HTTP WebSocket upgrade requests.The HTTPS server does not explicitly decode incoming secure WebSocket upgrade requests. Instead, it listens for these secure WebSocket requests and proxies them, leveraging the
node-http-proxylibrary's capability to handle WebSocket connections.Your separate HTTPS server is created using the provided SSL/TLS options. That server listens for "
upgrade" events, which occur when a client wants to initiate a WebSocket connection.When an "
upgrade" event is received, it invokesproxy.ws(), as illustrated innode-http-proxy/ UPGRADING, and handles the process of proxying the WebSocket request to the target specified when the proxy was created.So the client is trying to establish a non-secure WebSocket connection (
ws://) to the secure server (localhost:8443). That will not work because thelocalhost:8443server is set up to only accept secure WebSocket (wss://) connections due to the presence of the SSL/TLS configuration.You will need to modify the client side to initiate a secure WebSocket connection. If the client is a web browser, it should use
wss://localhost:8443instead ofws://localhost:8443when creating a new WebSocket connection.The correct protocol (
ws://for HTTP andwss://for HTTPS) should be used depending on whether the connection needs to be secure or not.