Skip to content

Commit 290235c

Browse files
committed
handle websocket requests to non-websocket targets
relay response status and body back to requester
1 parent 464fd1a commit 290235c

File tree

2 files changed

+108
-1
lines changed

2 files changed

+108
-1
lines changed

lib/http-proxy/passes/ws-incoming.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The names of passes are exported as WS_PASSES from this module.
1111
import * as http from "node:http";
1212
import * as https from "node:https";
1313
import * as common from "../common";
14-
import type { Request } from "./web-incoming";
14+
import type { Request, ProxyResponse } from "./web-incoming";
1515
import type { Socket } from "node:net";
1616
import debug from "debug";
1717
import type { NormalizedServerOptions, ProxyServer } from "..";
@@ -219,6 +219,28 @@ export function stream(
219219
// which may be another leak type situation and definitely doesn't work for unit testing.
220220
socket.destroySoon();
221221
}
222+
223+
// if we get a response, backend is not a websocket endpoint,
224+
// relay HTTP response and close the socket
225+
proxyReq.on("response", (proxyRes: ProxyResponse) => {
226+
log("got non-ws HTTP response",
227+
{
228+
statusCode: proxyRes.statusCode,
229+
statusMessage: proxyRes.statusMessage,
230+
}
231+
);
232+
const proxyHead = createHttpHeader(
233+
`HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}`,
234+
{"Connection": "close"},
235+
);
236+
if (!socket.destroyed) {
237+
socket.write(proxyHead);
238+
proxyRes.pipe(socket);
239+
} else {
240+
// make sure response is consumed
241+
proxyRes.resume();
242+
}
243+
});
222244

223245
proxyReq.end();
224246
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
pnpm test websocket-proxy-http.test.ts
3+
4+
Test clients connecting a websocket to a non-websocket backend.
5+
The connection should fail promptly and preserve the original error code.
6+
7+
See https://nodejs.org/api/http.html#event-response
8+
9+
DEVELOPMENT:
10+
11+
pnpm test websocket-proxy-http.test.ts
12+
*/
13+
14+
import * as http from "node:http";
15+
import * as httpProxy from "../..";
16+
import log from "../log";
17+
import getPort from "../get-port";
18+
import { once } from "node:events";
19+
import {describe, it, expect, beforeAll, afterAll} from "vitest";
20+
21+
describe("Example of client requesting websocket when backend is plain http", () => {
22+
let ports: Record<'httpOnly' | 'proxy', number>;
23+
beforeAll(async () => {
24+
// assigns ports
25+
ports = { httpOnly: await getPort(), proxy: await getPort() };
26+
});
27+
28+
let servers: any = {};
29+
30+
it("Create an http server that doesn't support websockets", async () => {
31+
const server = http.createServer((_req, res) => {
32+
res.writeHead(418, { "Content-Type": "text/plain" });
33+
res.end("not a websocket!");
34+
});
35+
36+
servers.httpOnly = server;
37+
server.listen(ports.httpOnly);
38+
});
39+
40+
it("Try a websocket client connecting to a regular HTTP backend", async () => {
41+
const options = {
42+
port: ports.httpOnly,
43+
host: "localhost",
44+
headers: {
45+
Connection: "Upgrade",
46+
Upgrade: "websocket",
47+
},
48+
};
49+
const req = http.request(options);
50+
req.end();
51+
const [res] = await once(req, "response");
52+
expect(res.statusCode).toEqual(418);
53+
const body = await res.read().toString();
54+
expect(body.trim()).toEqual("not a websocket!");
55+
log("we got an http response.");
56+
});
57+
58+
it("Create a proxy server pointed at the non-websocket server, expecting websockets", async () => {
59+
servers.proxy = httpProxy
60+
.createServer({ target: `ws://localhost:${ports.httpOnly}`, ws: true })
61+
.listen(ports.proxy);
62+
});
63+
64+
it("Create a websocket client and test the proxy server", async () => {
65+
const options = {
66+
port: ports.proxy,
67+
host: "localhost",
68+
headers: {
69+
Connection: "Upgrade",
70+
Upgrade: "websocket",
71+
},
72+
};
73+
const req = http.request(options);
74+
req.end();
75+
const [res] = await once(req, "response");
76+
expect(res.statusCode).toEqual(418);
77+
const body = await res.read().toString();
78+
expect(body.trim()).toEqual("not a websocket!");
79+
});
80+
81+
afterAll(async () => {
82+
// cleans up
83+
Object.values(servers).map((x: any) => x?.close());
84+
});
85+
});

0 commit comments

Comments
 (0)