Skip to content
File

Blob: src/workerd/api/tests/websocket-allow-half-open-test.js

javascript153 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import { strictEqual, doesNotThrow } from 'node:assert';
6 
7// Test that when allowHalfOpen is false (default with compat flag), a server-initiated
8// close sets readyState to CLOSED.
9export const autoCloseReplyWhenNotHalfOpen = {
10 async test() {
11 const pair = new WebSocketPair();
12 const [client, server] = Object.values(pair);
13 
14 // accept() without options — allowHalfOpen defaults to false with the compat flag.
15 client.accept();
16 server.accept();
17 
18 const closePromise = new Promise((resolve) => {
19 client.addEventListener('close', (event) => {
20 // When allowHalfOpen is false, the runtime auto-sends a close reply,
21 // so both closedIncoming and closedOutgoing are true — readyState should be CLOSED.
22 resolve({
23 readyState: client.readyState,
24 code: event.code,
25 wasClean: event.wasClean,
26 });
27 });
28 });
29 
30 // Server initiates close.
31 server.close(1000, 'server closing');
32 
33 const result = await closePromise;
34 strictEqual(result.readyState, WebSocket.CLOSED);
35 strictEqual(result.code, 1000);
36 strictEqual(result.wasClean, true);
37 },
38};
39 
40// Test that calling close() inside the close event handler after the automatic close
41// handshake is silently ignored (doesn't throw). This is the realistic pattern for
42// users who are already manually replying to close frames today — when they update
43// their compat date and get web_socket_auto_reply_to_close, their existing close()
44// call must not break.
45export const closeInsideHandlerAfterAutoCloseIsIgnored = {
46 async test() {
47 const pair = new WebSocketPair();
48 const [client, server] = Object.values(pair);
49 
50 client.accept();
51 server.accept();
52 
53 const closePromise = new Promise((resolve) => {
54 client.addEventListener('close', (event) => {
55 // This is what existing user code typically looks like: replying to close
56 // inside the close handler. With the auto-reply already sent, closedOutgoing
57 // is true but the native state is still Accepted (tryReleaseNative hasn't
58 // run yet), so this exercises the closedOutgoing early-return in close().
59 doesNotThrow(() => {
60 client.close(1000, 'manual reply inside handler');
61 });
62 
63 resolve({
64 readyState: client.readyState,
65 code: event.code,
66 wasClean: event.wasClean,
67 });
68 });
69 });
70 
71 // Server initiates close; the runtime auto-replies because allowHalfOpen is false.
72 server.close(1000, 'server closing');
73 
74 const result = await closePromise;
75 strictEqual(result.readyState, WebSocket.CLOSED);
76 strictEqual(result.code, 1000);
77 strictEqual(result.wasClean, true);
78 },
79};
80 
81// Same as above, but calling close() after the handler has returned and the native
82// WebSocket has been released. This exercises the state.is<Released>() early-return
83// in close(), which is a different code path from the closedOutgoing check above.
84export const closeAfterAutoCloseAndReleaseIsIgnored = {
85 async test() {
86 const pair = new WebSocketPair();
87 const [client, server] = Object.values(pair);
88 
89 client.accept();
90 server.accept();
91 
92 const closePromise = new Promise((resolve) => {
93 client.addEventListener('close', (event) => {
94 resolve({
95 readyState: client.readyState,
96 code: event.code,
97 wasClean: event.wasClean,
98 });
99 });
100 });
101 
102 // Server initiates close; the runtime auto-replies because allowHalfOpen is false.
103 server.close(1000, 'server closing');
104 
105 const result = await closePromise;
106 strictEqual(result.readyState, WebSocket.CLOSED);
107 
108 // By now tryReleaseNative has run and the state is Released.
109 // close() should still be silently ignored.
110 doesNotThrow(() => {
111 client.close(1000, 'manual reply after release');
112 });
113 
114 strictEqual(client.readyState, WebSocket.CLOSED);
115 },
116};
117 
118// Test that when allowHalfOpen is true via accept(), a server-initiated close sets
119// readyState to CLOSING.
120export const halfOpenCloseKeepsClosingState = {
121 async test() {
122 const pair = new WebSocketPair();
123 const [client, server] = Object.values(pair);
124 
125 // Opt into half-open mode via accept() options.
126 client.accept({ allowHalfOpen: true });
127 server.accept();
128 
129 const closePromise = new Promise((resolve) => {
130 client.addEventListener('close', (event) => {
131 // When allowHalfOpen is true, no auto-reply is sent, so only closedIncoming
132 // is true — readyState should be CLOSING.
133 resolve({
134 readyState: client.readyState,
135 code: event.code,
136 wasClean: event.wasClean,
137 });
138 });
139 });
140 
141 // Server initiates close.
142 server.close(1000, 'server closing');
143 
144 const result = await closePromise;
145 strictEqual(result.readyState, WebSocket.CLOSING);
146 strictEqual(result.code, 1000);
147 strictEqual(result.wasClean, true);
148 
149 // The client must manually close to complete the handshake.
150 client.close(1000, 'client reply');
151 },
152};