From 3752acffe0f43e6efe7dccbbdd0bb535bb881c36 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 21 Dec 2021 17:59:09 +0000 Subject: [PATCH] websocket support in `dev` (#150) * websocket support This resuscitates https://github.com/threepointone/nu-wrangler/pull/19 via @Electroid. This probably needs tests, in a future PR. But it works well! * Add a changeset * update changeset --- .changeset/fuzzy-dancers-nail.md | 5 + package-lock.json | 174 +++++++++++++++++++------------ packages/wrangler/package.json | 3 +- packages/wrangler/src/dev.tsx | 62 ++++++----- packages/wrangler/src/proxy.ts | 104 ++++++++++++++++++ 5 files changed, 253 insertions(+), 95 deletions(-) create mode 100644 .changeset/fuzzy-dancers-nail.md create mode 100644 packages/wrangler/src/proxy.ts diff --git a/.changeset/fuzzy-dancers-nail.md b/.changeset/fuzzy-dancers-nail.md new file mode 100644 index 000000000000..71cd6a9b43a9 --- /dev/null +++ b/.changeset/fuzzy-dancers-nail.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Add support for websockets in `dev`, i.e. when developing workers. This replaces the proxy layer that we use to connect to the 'edge' during preview mode, using the `faye-wesocket` library. diff --git a/package-lock.json b/package-lock.json index 23b5902d1c76..383a5069b89a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2805,6 +2805,12 @@ "version": "7.0.9", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.2", "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" @@ -2845,6 +2851,16 @@ "version": "6.2.3", "integrity": "sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==" }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/signal-exit": { "version": "3.0.1", "integrity": "sha512-OSitN9PP9E/c4tlt1Qdj3CAz5uHD9Da5rhUqlaKyQRCX1T7Zdpbk6YdeZbR2eiE2ce+NMBgVnMxGqpaPSNQDUQ==", @@ -4926,11 +4942,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, "node_modules/example-worker-app": { "resolved": "packages/example-worker-app", "link": true @@ -5305,6 +5316,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.1", "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", @@ -5443,25 +5466,6 @@ "version": "3.2.4", "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==" }, - "node_modules/follow-redirects": { - "version": "1.14.5", - "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-in": { "version": "1.0.2", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", @@ -5833,18 +5837,11 @@ "node": ">= 0.6" } }, - "node_modules/http-proxy": { - "version": "1.18.1", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } + "node_modules/http-parser-js": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.5.tgz", + "integrity": "sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA==", + "dev": true }, "node_modules/http-proxy-agent": { "version": "4.0.1", @@ -11206,11 +11203,6 @@ "version": "2.0.0", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "node_modules/requires-port": { - "version": "1.0.0", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, "node_modules/resolve": { "version": "2.0.0-next.3", "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", @@ -13178,6 +13170,29 @@ "node": ">=10.4" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "1.0.5", "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", @@ -13452,6 +13467,7 @@ "devDependencies": { "@iarna/toml": "^2.2.5", "@types/react": "^17.0.37", + "@types/serve-static": "^1.13.10", "@types/signal-exit": "^3.0.1", "@types/ws": "^8.2.1", "@types/yargs": "^17.0.7", @@ -13459,10 +13475,10 @@ "clipboardy": "^3.0.0", "command-exists": "^1.2.9", "execa": "^6.0.0", + "faye-websocket": "^0.11.4", "finalhandler": "^1.1.2", "find-up": "^6.2.0", "formdata-node": "^4.3.1", - "http-proxy": "^1.18.1", "ink": "^3.2.0", "ink-select-input": "^4.2.1", "ink-table": "^3.0.0", @@ -15691,6 +15707,12 @@ "version": "7.0.9", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, "@types/minimist": { "version": "1.2.2", "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" @@ -15731,6 +15753,16 @@ "version": "6.2.3", "integrity": "sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==" }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "@types/signal-exit": { "version": "3.0.1", "integrity": "sha512-OSitN9PP9E/c4tlt1Qdj3CAz5uHD9Da5rhUqlaKyQRCX1T7Zdpbk6YdeZbR2eiE2ce+NMBgVnMxGqpaPSNQDUQ==", @@ -17172,11 +17204,6 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, - "eventemitter3": { - "version": "4.0.7", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, "example-worker-app": { "version": "file:packages/example-worker-app" }, @@ -17465,6 +17492,15 @@ "reusify": "^1.0.4" } }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, "fb-watchman": { "version": "2.0.1", "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", @@ -17567,11 +17603,6 @@ "version": "3.2.4", "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==" }, - "follow-redirects": { - "version": "1.14.5", - "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==", - "dev": true - }, "for-in": { "version": "1.0.2", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" @@ -17829,15 +17860,11 @@ "toidentifier": "1.0.0" } }, - "http-proxy": { - "version": "1.18.1", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } + "http-parser-js": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.5.tgz", + "integrity": "sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA==", + "dev": true }, "http-proxy-agent": { "version": "4.0.1", @@ -21640,11 +21667,6 @@ "version": "2.0.0", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "requires-port": { - "version": "1.0.0", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, "resolve": { "version": "2.0.0-next.3", "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", @@ -23137,6 +23159,23 @@ "version": "6.1.0", "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, "whatwg-encoding": { "version": "1.0.5", "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", @@ -23211,6 +23250,7 @@ "@cloudflare/pages-functions-compiler": "0.3.8", "@iarna/toml": "^2.2.5", "@types/react": "^17.0.37", + "@types/serve-static": "^1.13.10", "@types/signal-exit": "^3.0.1", "@types/ws": "^8.2.1", "@types/yargs": "^17.0.7", @@ -23219,11 +23259,11 @@ "command-exists": "^1.2.9", "esbuild": "0.14.1", "execa": "^6.0.0", + "faye-websocket": "^0.11.4", "finalhandler": "^1.1.2", "find-up": "^6.2.0", "formdata-node": "^4.3.1", "fsevents": "~2.3.2", - "http-proxy": "^1.18.1", "ink": "^3.2.0", "ink-select-input": "^4.2.1", "ink-table": "^3.0.0", diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 1835021820fb..3e1f42eca7ea 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -47,6 +47,7 @@ "devDependencies": { "@iarna/toml": "^2.2.5", "@types/react": "^17.0.37", + "@types/serve-static": "^1.13.10", "@types/signal-exit": "^3.0.1", "@types/ws": "^8.2.1", "@types/yargs": "^17.0.7", @@ -54,10 +55,10 @@ "clipboardy": "^3.0.0", "command-exists": "^1.2.9", "execa": "^6.0.0", + "faye-websocket": "^0.11.4", "finalhandler": "^1.1.2", "find-up": "^6.2.0", "formdata-node": "^4.3.1", - "http-proxy": "^1.18.1", "ink": "^3.2.0", "ink-select-input": "^4.2.1", "ink-table": "^3.0.0", diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index a8f567ba9591..b12866eeb54b 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -1,5 +1,4 @@ import esbuild from "esbuild"; -import httpProxy from "http-proxy"; import { readFile } from "fs/promises"; import type { DirectoryResult } from "tmp-promise"; import tmp from "tmp-promise"; @@ -24,6 +23,7 @@ import { getAPIToken } from "./user"; import fetch from "node-fetch"; import makeModuleCollector from "./module-collection"; import { withErrorBoundary, useErrorHandler } from "react-error-boundary"; +import { createHttpProxy } from "./proxy"; type CfScriptFormat = void | "modules" | "service-worker"; @@ -565,43 +565,51 @@ function useProxy({ }) { useEffect(() => { if (!token) return; - const proxy = httpProxy.createProxyServer({ - secure: false, - changeOrigin: true, - headers: { - "cf-workers-preview-token": token.value, + // TODO(soon): since headers are added in callbacks, the server + // does not need to restart when changes are made. + const host = token.host; + const proxy = createHttpProxy({ + host, + assetPath: typeof publicRoot === "string" ? publicRoot : null, + onRequest: (headers) => { + headers["cf-workers-preview-token"] = token.value; + }, + onResponse: (headers) => { + for (const [name, value] of Object.entries(headers)) { + // Rewrite the remote host to the local host. + if (typeof value === "string" && value.includes(host)) { + headers[name] = value + .replaceAll(`https://${host}`, `http://localhost:${port}`) + .replaceAll(host, `localhost:${port}`); + } + } }, - target: `https://${token.host}`, - // TODO: log websockets too? validate durables, etc }); - const servePublic = - publicRoot && - serveStatic(publicRoot, { - cacheControl: false, - }); - const server = http - .createServer((req, res) => { - if (publicRoot) { - servePublic(req, res, () => { - proxy.web(req, res); - }); - } else { - proxy.web(req, res); - } - }) - .listen(port); // TODO: custom port + const server = proxy.listen(port); - proxy.on("proxyRes", function (proxyRes, req, res) { + // TODO(soon): refactor logging format into its own function + proxy.on("request", function (req, res) { // log all requests console.log( new Date().toLocaleTimeString(), req.method, req.url, - res.statusCode // TODO add a status message like Ok etc? + res.statusCode + ); + }); + proxy.on("upgrade", (req) => { + console.log( + new Date().toLocaleTimeString(), + req.method, + req.url, + 101, + "(WebSocket)" ); }); - // TODO: log errors? + proxy.on("error", (err) => { + console.error(new Date().toLocaleTimeString(), err); + }); return () => { proxy.close(); diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts new file mode 100644 index 000000000000..b3a8d7ac0f30 --- /dev/null +++ b/packages/wrangler/src/proxy.ts @@ -0,0 +1,104 @@ +import { connect } from "node:http2"; +import { createServer } from "node:http"; +import type { + Server, + IncomingHttpHeaders, + OutgoingHttpHeaders, + RequestListener, +} from "node:http"; +import WebSocket from "faye-websocket"; +import serveStatic from "serve-static"; + +export interface HttpProxyInit { + host: string; + assetPath?: string | null; + onRequest?: (headers: IncomingHttpHeaders) => void; + onResponse?: (headers: OutgoingHttpHeaders) => void; +} + +/** + * Creates a HTTP/1 proxy that sends requests over HTTP/2. + */ +export function createHttpProxy(init: HttpProxyInit): Server { + const { host, assetPath, onRequest = () => {}, onResponse = () => {} } = init; + const remote = connect(`https://${host}`); + const local = createServer(); + // HTTP/2 -> HTTP/2 + local.on("stream", (stream, headers: IncomingHttpHeaders) => { + onRequest(headers); + headers[":authority"] = host; + const request = stream.pipe(remote.request(headers)); + request.on("response", (headers: OutgoingHttpHeaders) => { + onResponse(headers); + stream.respond(headers); + request.pipe(stream, { end: true }); + }); + }); + // HTTP/1 -> HTTP/2 + const handleRequest: RequestListener = (message, response) => { + const { httpVersionMajor, headers, method, url } = message; + if (httpVersionMajor >= 2) { + return; // Already handled by the "stream" event. + } + onRequest(headers); + headers[":method"] = method; + headers[":path"] = url; + headers[":authority"] = host; + headers[":scheme"] = "https"; + for (const name of Object.keys(headers)) { + if (HTTP1_HEADERS.has(name.toLowerCase())) { + delete headers[name]; + } + } + const request = message.pipe(remote.request(headers)); + request.on("response", (headers) => { + const status = headers[":status"]; + onResponse(headers); + for (const name of Object.keys(headers)) { + if (name.startsWith(":")) { + delete headers[name]; + } + } + response.writeHead(status, headers); + request.pipe(response, { end: true }); + }); + }; + // If an asset path is defined, check the file system + // for a file first and serve if it exists. + if (assetPath) { + const handleAsset = serveStatic(assetPath, { + cacheControl: false, + }); + local.on("request", (request, response) => { + handleAsset(request, response, () => { + handleRequest(request, response); + }); + }); + } else { + local.on("request", handleRequest); + } + // HTTP/1 -> WebSocket (over HTTP/1) + local.on("upgrade", (message, socket, body) => { + const { headers, url } = message; + onRequest(headers); + headers["host"] = host; + const local = new WebSocket(message, socket, body); + // TODO(soon): Custom WebSocket protocol is not working? + const remote = new WebSocket.Client(`wss://${host}${url}`, [], { headers }); + local.pipe(remote).pipe(local); + }); + remote.on("close", () => { + local.close(); + }); + return local; +} + +const HTTP1_HEADERS = new Set([ + "host", + "connection", + "upgrade", + "keep-alive", + "proxy-connection", + "transfer-encoding", + "http2-settings", +]);