11import { createServer , type Server } from "node:http" ;
22import { afterEach , describe , expect , it } from "vitest" ;
3- import { WebSocketServer , type WebSocket } from "ws" ;
3+ import { WebSocket , WebSocketServer } from "ws" ;
44import { createGatewayWsClient } from "../../scripts/dev/gateway-ws-client.js" ;
55
66let server : Server | undefined ;
@@ -44,6 +44,36 @@ async function listen(handler: (ws: WebSocket) => void): Promise<string> {
4444 return `ws://127.0.0.1:${ address . port } ` ;
4545}
4646
47+ async function listenStalledUpgrade ( ) : Promise < { close : ( ) => Promise < void > ; url : string } > {
48+ const stalledServer = createServer ( ) ;
49+ const sockets = new Set < import ( "node:net" ) . Socket > ( ) ;
50+ stalledServer . on ( "upgrade" , ( _req , socket ) => {
51+ // Keep the socket open without completing the websocket handshake.
52+ sockets . add ( socket ) ;
53+ socket . once ( "close" , ( ) => {
54+ sockets . delete ( socket ) ;
55+ } ) ;
56+ } ) ;
57+ await new Promise < void > ( ( resolve ) => {
58+ stalledServer . listen ( 0 , "127.0.0.1" , resolve ) ;
59+ } ) ;
60+ const address = stalledServer . address ( ) ;
61+ if ( ! address || typeof address === "string" ) {
62+ throw new Error ( "test websocket server did not get a TCP address" ) ;
63+ }
64+ return {
65+ close : async ( ) => {
66+ for ( const socket of sockets ) {
67+ socket . destroy ( ) ;
68+ }
69+ await new Promise < void > ( ( resolve , reject ) => {
70+ stalledServer . close ( ( error ) => ( error ? reject ( error ) : resolve ( ) ) ) ;
71+ } ) ;
72+ } ,
73+ url : `ws://127.0.0.1:${ address . port } ` ,
74+ } ;
75+ }
76+
4777describe ( "createGatewayWsClient" , ( ) => {
4878 it ( "rejects pending RPC requests when the client closes" , async ( ) => {
4979 const url = await listen ( ( ) => { } ) ;
@@ -70,4 +100,28 @@ describe("createGatewayWsClient", () => {
70100 ) ;
71101 client . close ( ) ;
72102 } ) ;
103+
104+ it ( "terminates stalled websocket handshakes after the open timeout" , async ( ) => {
105+ const stalled = await listenStalledUpgrade ( ) ;
106+ const client = createGatewayWsClient ( { openTimeoutMs : 5 , url : stalled . url } ) ;
107+ try {
108+ await expect ( client . waitOpen ( ) ) . rejects . toThrow ( "ws open timeout" ) ;
109+ await waitFor ( ( ) => client . ws . readyState === WebSocket . CLOSED ) ;
110+ } finally {
111+ client . close ( ) ;
112+ await stalled . close ( ) ;
113+ }
114+ } ) ;
73115} ) ;
116+
117+ async function waitFor ( condition : ( ) => boolean , timeoutMs = 1_000 ) {
118+ const startedAt = Date . now ( ) ;
119+ while ( ! condition ( ) ) {
120+ if ( Date . now ( ) - startedAt > timeoutMs ) {
121+ throw new Error ( "timed out waiting for condition" ) ;
122+ }
123+ await new Promise ( ( resolve ) => {
124+ setTimeout ( resolve , 10 ) ;
125+ } ) ;
126+ }
127+ }
0 commit comments