@@ -57,6 +57,7 @@ WebSocket::WebSocket(
5757 binaryType_ (FeatureFlags::get(js).getWebsocketBinaryTypeDefault() ? BinaryType::BLOB
5858 : BinaryType::ARRAYBUFFER),
5959 serializedAttachment (kj::mv(package.serializedAttachment)),
60+ allowHalfOpen (package.allowHalfOpen),
6061 farNative (initNative(ioContext,
6162 ws,
6263 kj::mv (KJ_REQUIRE_NONNULL(package.maybeTags)),
@@ -76,6 +77,7 @@ WebSocket::WebSocket(jsg::Lock& js, kj::Own<kj::WebSocket> native)
7677 url(kj::none),
7778 binaryType_(FeatureFlags::get(js).getWebsocketBinaryTypeDefault() ? BinaryType::BLOB
7879 : BinaryType::ARRAYBUFFER),
80+ allowHalfOpen(!FeatureFlags::get(js).getWebSocketCloseReadyStateClosed()),
7981 farNative(nullptr ),
8082 outgoingMessages(IoContext::current().addObject(kj::heap<OutgoingMessagesMap>())) {
8183 auto nativeObj = kj::heap<Native>();
@@ -88,6 +90,7 @@ WebSocket::WebSocket(jsg::Lock& js, kj::String url)
8890 url(kj::mv(url)),
8991 binaryType_(FeatureFlags::get(js).getWebsocketBinaryTypeDefault() ? BinaryType::BLOB
9092 : BinaryType::ARRAYBUFFER),
93+ allowHalfOpen(!FeatureFlags::get(js).getWebSocketCloseReadyStateClosed()),
9194 farNative(nullptr ),
9295 outgoingMessages(IoContext::current().addObject(kj::heap<OutgoingMessagesMap>())) {
9396 auto nativeObj = kj::heap<Native>();
@@ -605,11 +608,8 @@ void WebSocket::send(jsg::Lock& js, kj::OneOf<kj::Array<byte>, kj::String> messa
605608 KJ_UNREACHABLE;
606609 }();
607610
608- auto pendingAutoResponses =
609- autoResponseStatus.pendingAutoResponseDeque .size () - autoResponseStatus.queuedAutoResponses ;
610- autoResponseStatus.queuedAutoResponses = autoResponseStatus.pendingAutoResponseDeque .size ();
611611 outgoingMessages->insert (
612- GatedMessage{kj::mv (maybeOutputLock), kj::mv (msg), pendingAutoResponses });
612+ GatedMessage{kj::mv (maybeOutputLock), kj::mv (msg), getPendingAutoResponseCount () });
613613
614614 ensurePumping (js);
615615}
@@ -680,22 +680,13 @@ void WebSocket::close(
680680
681681 assertNoError (js);
682682
683- // pendingAutoResponses stores the number of queuedAutoResponses that will be pumped before sending
684- // the current GatedMessage, guaranteeing order.
685- // queuedAutoResponses stores the total number of auto-response messages that are already in accounted
686- // for in previous GatedMessages. This is useful to easily calculate the number of pendingAutoResponses
687- // for each new GateMessage.
688- auto pendingAutoResponses =
689- autoResponseStatus.pendingAutoResponseDeque .size () - autoResponseStatus.queuedAutoResponses ;
690- autoResponseStatus.queuedAutoResponses = autoResponseStatus.pendingAutoResponseDeque .size ();
691-
692683 outgoingMessages->insert (GatedMessage{IoContext::current ().waitForOutputLocksIfNecessary (),
693684 kj::WebSocket::Close{
694685 // Code 1005 actually translates to sending a close message with no body on the wire.
695686 static_cast <uint16_t >(code.orDefault (1005 )),
696687 kj::mv (reason).orDefault (jsg::USVString (kj::str ())),
697688 },
698- pendingAutoResponses });
689+ getPendingAutoResponseCount () });
699690
700691 native.closedOutgoing = true ;
701692 closedOutgoingForHib = true ;
@@ -716,6 +707,14 @@ int WebSocket::getReadyState() {
716707 return READY_STATE_OPEN;
717708}
718709
710+ bool WebSocket::getAllowHalfOpen () {
711+ return allowHalfOpen.toBool ();
712+ }
713+
714+ void WebSocket::setAllowHalfOpen (bool value) {
715+ allowHalfOpen = AllowHalfOpen (value);
716+ }
717+
719718bool WebSocket::isAccepted () {
720719 return farNative->state .is <Accepted>();
721720}
@@ -990,6 +989,13 @@ kj::Promise<void> WebSocket::pump(IoContext& context,
990989 completed = true ;
991990}
992991
992+ size_t WebSocket::getPendingAutoResponseCount () {
993+ auto count =
994+ autoResponseStatus.pendingAutoResponseDeque .size () - autoResponseStatus.queuedAutoResponses ;
995+ autoResponseStatus.queuedAutoResponses = autoResponseStatus.pendingAutoResponseDeque .size ();
996+ return count;
997+ }
998+
993999void WebSocket::tryReleaseNative (jsg::Lock& js) {
9941000 // If the native WebSocket is no longer needed (the connection closed) and there are no more
9951001 // messages to send, we can discard the underlying connection.
@@ -1057,6 +1063,23 @@ kj::Promise<kj::Maybe<kj::Exception>> WebSocket::readLoop(
10571063 }
10581064 KJ_CASE_ONEOF (close, kj::WebSocket::Close) {
10591065 native.closedIncoming = true ;
1066+ if (!allowHalfOpen.toBool () && !native.closedOutgoing && !native.outgoingAborted &&
1067+ !native.state .is <Released>()) {
1068+ // When allowHalfOpen is false (the spec-compliant default with the
1069+ // no_web_socket_half_open_by_default compat flag), automatically send a reciprocal
1070+ // Close frame through the outgoing message pump so that readyState is CLOSED (3)
1071+ // when the close event fires. Skip if a close frame was already sent (e.g. the
1072+ // application called close() before the server sent its Close), or if the outgoing
1073+ // side is otherwise unusable.
1074+ outgoingMessages->insert (
1075+ GatedMessage{IoContext::current ().waitForOutputLocksIfNecessary (),
1076+ kj::WebSocket::Close{close.code , kj::str (close.reason )},
1077+ getPendingAutoResponseCount ()});
1078+
1079+ native.closedOutgoing = true ;
1080+ closedOutgoingForHib = true ;
1081+ ensurePumping (js);
1082+ }
10601083 dispatchEventImpl (js, js.alloc <CloseEvent>(close.code , kj::mv (close.reason ), true ));
10611084 // Native WebSocket no longer needed; release.
10621085 tryReleaseNative (js);
@@ -1202,6 +1225,7 @@ WebSocket::HibernationPackage WebSocket::buildPackageForHibernation() {
12021225 .serializedAttachment = kj::mv (serializedAttachment),
12031226 .maybeTags = kj::none,
12041227 .closedOutgoingConnection = closedOutgoingForHib,
1228+ .allowHalfOpen = allowHalfOpen,
12051229 };
12061230}
12071231
0 commit comments