From 4a4316a50a3c859e197f69913723edbdc5cefc9f Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 11 Feb 2025 14:50:14 +0530 Subject: [PATCH 01/15] Add initial implementation --- ballerina/close_frame_return_types.bal | 137 +++++++++++ ballerina/tests/close_frame_test.bal | 214 ++++++++++++++++++ .../stdlib/websocket/WebSocketConstants.java | 5 + .../WebSocketResourceDispatcher.java | 22 ++ 4 files changed, 378 insertions(+) create mode 100644 ballerina/close_frame_return_types.bal create mode 100644 ballerina/tests/close_frame_test.bal diff --git a/ballerina/close_frame_return_types.bal b/ballerina/close_frame_return_types.bal new file mode 100644 index 000000000..e6ea89aec --- /dev/null +++ b/ballerina/close_frame_return_types.bal @@ -0,0 +1,137 @@ +// Copyright (c) 2025 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +public type Status distinct object { + public int code; // Constraint minValue: 1000, maxValue: 4999 +}; + +type CloseFrame record {| + readonly Status status; + string reason?; +|}; + +public const NORMAL_CLOSURE_STATUS_CODE = 1000; +public const GOING_AWAY_STATUS_CODE = 1001; +// public const PROTOCOL_ERROR_STATUS_CODE = 1002; +public const UNSUPPORTED_DATA_STATUS_CODE = 1003; +public const INVALID_PAYLOAD_STATUS_CODE = 1007; +public const POLICY_VIOLATION_STATUS_CODE = 1008; +public const MESSAGE_TOO_BIG_STATUS_CODE = 1009; +public const INTERNAL_SERVER_ERROR_STATUS_CODE = 1011; + +public readonly distinct class NormalClosureStatus { + *Status; + public int code = NORMAL_CLOSURE_STATUS_CODE; +} + +public readonly distinct class GoingAwayStatus { + *Status; + public int code = GOING_AWAY_STATUS_CODE; +} + +// public readonly distinct class ProtocolErrorStatus { +// *Status; +// public int code = PROTOCOL_ERROR_STATUS_CODE; +// } + +public readonly distinct class UnsupportedDataStatus { + *Status; + public int code = UNSUPPORTED_DATA_STATUS_CODE; +} + +public readonly distinct class InvalidPayloadStatus { + *Status; + public int code = INVALID_PAYLOAD_STATUS_CODE; +} + +public readonly distinct class PolicyViolationStatus { + *Status; + public int code = POLICY_VIOLATION_STATUS_CODE; +} + +public readonly distinct class MessageTooBigStatus { + *Status; + public int code = MESSAGE_TOO_BIG_STATUS_CODE; +} + +public readonly distinct class InternalServerErrorStatus { + *Status; + public int code = INTERNAL_SERVER_ERROR_STATUS_CODE; +} + +public final NormalClosureStatus NORMAL_CLOSURE_STATUS_OBJ = new; +public final GoingAwayStatus GOING_AWAY_STATUS_OBJ = new; +// public final ProtocolErrorStatus PROTOCOL_ERROR_STATUS_OBJ = new; +public final UnsupportedDataStatus UNSUPPORTED_DATA_STATUS_OBJ = new; +public final InvalidPayloadStatus INVALID_PAYLOAD_STATUS_OBJ = new; +public final PolicyViolationStatus POLICY_VIOLATION_STATUS_OBJ = new; +public final MessageTooBigStatus MESSAGE_TOO_BIG_STATUS_OBJ = new; +public final InternalServerErrorStatus INTERNAL_SERVER_ERROR_STATUS_OBJ = new; + +public type NormalClosure record {| + *CloseFrame; + readonly NormalClosureStatus status = NORMAL_CLOSURE_STATUS_OBJ; +|}; + +public type GoingAway record {| + *CloseFrame; + readonly GoingAwayStatus status = GOING_AWAY_STATUS_OBJ; +|}; + +// public type ProtocolError record {| +// *CloseFrame; +// readonly ProtocolErrorStatus status = PROTOCOL_ERROR_STATUS_OBJ; +// string reason = "Connection closed due to protocol error"; +// |}; + +public type UnsupportedData record {| + *CloseFrame; + readonly UnsupportedDataStatus status = UNSUPPORTED_DATA_STATUS_OBJ; + string reason = "Endpoint received unsupported frame"; +|}; + +public type InvalidPayload record {| + *CloseFrame; + readonly InvalidPayloadStatus status = INVALID_PAYLOAD_STATUS_OBJ; + string reason = "Payload does not match the expected format or encoding"; +|}; + +public type PolicyViolation record {| + *CloseFrame; + readonly PolicyViolationStatus status = POLICY_VIOLATION_STATUS_OBJ; + string reason = "Received message violates its policy"; +|}; + +public type MessageTooBig record {| + *CloseFrame; + readonly MessageTooBigStatus status = MESSAGE_TOO_BIG_STATUS_OBJ; + string reason = "The received message exceeds the allowed size limit"; +|}; + +public type InternalServerError record {| + *CloseFrame; + readonly InternalServerErrorStatus status = INTERNAL_SERVER_ERROR_STATUS_OBJ; + string reason = "Internal server error occurred"; +|}; + +public final readonly & NormalClosure NORMAL_CLOSURE = {}; +public final readonly & GoingAway GOING_AWAY = {}; +// public final readonly & ProtocolError PROTOCOL_ERROR = {}; +public final readonly & UnsupportedData UNSUPPORTED_DATA = {}; +public final readonly & InvalidPayload INVALID_PAYLOAD = {}; +public final readonly & PolicyViolation POLICY_VIOLATION = {}; +public final readonly & MessageTooBig MESSAGE_TOO_BIG = {}; +public final readonly & InternalServerError INTERNAL_SERVER_ERROR = {}; diff --git a/ballerina/tests/close_frame_test.bal b/ballerina/tests/close_frame_test.bal new file mode 100644 index 000000000..b1445834d --- /dev/null +++ b/ballerina/tests/close_frame_test.bal @@ -0,0 +1,214 @@ +// Copyright (c) 2025 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; + +listener Listener l102 = new (22081); +listener Listener l103 = new (22082); +listener Listener l104 = new (22083); +listener Listener l105 = new (22084); +listener Listener l106 = new (22085); +listener Listener l107 = new (22086); +listener Listener l108 = new (22087); + +service /onCloseFrame on l102 { + resource function get .() returns Service|UpgradeError { + return new WsService102(); + } +} + +service /onCloseFrame on l103 { + resource function get .() returns Service|UpgradeError { + return new WsService103(); + } +} + +service /onCloseFrame on l104 { + resource function get .() returns Service|UpgradeError { + return new WsService104(); + } +} + +service /onCloseFrame on l105 { + resource function get .() returns Service|UpgradeError { + return new WsService105(); + } +} + +service /onCloseFrame on l106 { + resource function get .() returns Service|UpgradeError { + return new WsService106(); + } +} + +service /onCloseFrame on l107 { + resource function get .() returns Service|UpgradeError { + return new WsService107(); + } +} + +service /onCloseFrame on l108 { + resource function get .() returns Service|UpgradeError { + return new WsService108(); + } +} + +service class WsService102 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return NORMAL_CLOSURE; + } +} + +service class WsService103 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return GOING_AWAY; + } +} + +service class WsService104 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return UNSUPPORTED_DATA; + } +} + +service class WsService105 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return INVALID_PAYLOAD; + } +} + +service class WsService106 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return POLICY_VIOLATION; + } +} + +service class WsService107 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return MESSAGE_TOO_BIG; + } +} + +service class WsService108 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return INTERNAL_SERVER_ERROR; + } +} + +@test:Config { + groups: ["closeFrame"] +} +public function testNormalClosure() returns Error? { + Client wsClient = check new ("ws://localhost:22081/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if (res is Error) { + test:assertEquals(res.message(), "Connection closed Status code: 1000"); + } +} + +@test:Config { + groups: ["closeFrame"] +} +public function testGoingAway() returns Error? { + Client wsClient = check new ("ws://localhost:22082/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if (res is Error) { + test:assertEquals(res.message(), "Connection closed Status code: 1001"); + } +} + +@test:Config { + groups: ["closeFrame"] +} +public function testUnsupportedData() returns Error? { + Client wsClient = check new ("ws://localhost:22083/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if (res is Error) { + test:assertEquals(res.message(), "Endpoint received unsupported frame: Status code: 1003"); + } +} + +@test:Config { + groups: ["closeFrame"] +} +public function testInvalidPayload() returns Error? { + Client wsClient = check new ("ws://localhost:22084/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if (res is Error) { + test:assertEquals(res.message(), "Payload does not match the expected format or encoding: Status code: 1007"); + } +} + +@test:Config { + groups: ["closeFrame"] +} +public function testPolicyViolation() returns Error? { + Client wsClient = check new ("ws://localhost:22085/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if (res is Error) { + test:assertEquals(res.message(), "Received message violates its policy: Status code: 1008"); + } +} + +@test:Config { + groups: ["closeFrame"] +} +public function testMessageTooBig() returns Error? { + Client wsClient = check new ("ws://localhost:22086/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if (res is Error) { + test:assertEquals(res.message(), "The received message exceeds the allowed size limit: Status code: 1009"); + } +} + +@test:Config { + groups: ["closeFrame"] +} +public function testInternalServerError() returns Error? { + Client wsClient = check new ("ws://localhost:22087/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if (res is Error) { + test:assertEquals(res.message(), "Internal server error occurred: Status code: 1011"); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index e3ad7aa16..969f20691 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -139,6 +139,11 @@ public class WebSocketConstants { public static final String CONNECTION_CLOSED = "Connection closed"; public static final String STATUS_CODE = "Status code:"; + // Close Frame Records + public static final BString CLOSE_FRAME_STATUS = StringUtils.fromString("status"); + public static final BString CLOSE_FRAME_STATUS_CODE = StringUtils.fromString("code"); + public static final BString CLOSE_FRAME_REASON = StringUtils.fromString("reason"); + private WebSocketConstants() { } diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index c9a9ee6fb..b064f5c8e 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -1127,6 +1127,18 @@ private static void executeResource(WebSocketService wsService, BObject balservi } StrandMetadata strandMetadata = new StrandMetadata(isIsolated(balservice, resource), properties); result = wsService.getRuntime().callMethod(balservice, resource, strandMetadata, bValues); + if (isCloseFrameRecord(result)) { + @SuppressWarnings(WebSocketConstants.UNCHECKED) + BMap closeFrameRecord = (BMap) result; + long status = ((BObject) closeFrameRecord.get(WebSocketConstants.CLOSE_FRAME_STATUS)) + .getIntValue(WebSocketConstants.CLOSE_FRAME_STATUS_CODE); + BString reason = closeFrameRecord.containsKey(WebSocketConstants.CLOSE_FRAME_REASON) ? + (BString) closeFrameRecord.get(WebSocketConstants.CLOSE_FRAME_REASON) + : StringUtils.fromString(""); + result = wsService.getRuntime().callMethod( + connectionInfo.getWebSocketEndpoint(), WebSocketConstants.RESOURCE_NAME_CLOSE, + strandMetadata, status, reason); + } callback.notifySuccess(result); WebSocketObservabilityUtil.observeResourceInvocation(connectionInfo, resource); } catch (BError bError) { @@ -1135,6 +1147,16 @@ private static void executeResource(WebSocketService wsService, BObject balservi }); } + @SuppressWarnings(WebSocketConstants.UNCHECKED) + private static boolean isCloseFrameRecord(Object obj) { + if (obj instanceof BMap) { + BMap bMap = (BMap) obj; + return bMap.containsKey(WebSocketConstants.CLOSE_FRAME_STATUS) && + bMap.get(WebSocketConstants.CLOSE_FRAME_STATUS) instanceof BObject; + } + return false; + } + private static boolean isIsolated(BObject serviceObj, String remoteMethod) { ObjectType serviceObjType = (ObjectType) TypeUtils.getReferredType(serviceObj.getType()); return serviceObjType.isIsolated() && serviceObjType.isIsolated(remoteMethod); From 42a174b15cb63b06da01138c198ca39eeb6cdc0c Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 11 Feb 2025 16:40:53 +0530 Subject: [PATCH 02/15] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 8 ++++---- ballerina/CompilerPlugin.toml | 2 +- ballerina/Dependencies.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 0d3a895f9..1ef793c14 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "websocket" -version = "2.13.1" +version = "2.13.2" authors = ["Ballerina"] keywords = ["ws", "network", "bi-directional", "streaming", "service", "client"] repository = "https://github.com/ballerina-platform/module-ballerina-websocket" @@ -15,8 +15,8 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "websocket-native" -version = "2.13.1" -path = "../native/build/libs/websocket-native-2.13.1.jar" +version = "2.13.2" +path = "../native/build/libs/websocket-native-2.13.2-SNAPSHOT.jar" [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" @@ -85,5 +85,5 @@ version = "4.1.118.Final" path = "./lib/netty-handler-proxy-4.1.118.Final.jar" [[platform.java21.dependency]] -path = "../test-utils/build/libs/websocket-test-utils-2.13.1.jar" +path = "../test-utils/build/libs/websocket-test-utils-2.13.2-SNAPSHOT.jar" scope = "testOnly" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index ab54a682e..4920708ae 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "websocket-compiler-plugin" class = "io.ballerina.stdlib.websocket.plugin.WebSocketCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.13.1.jar" +path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.13.2-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 331a030a6..dd90c575d 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -342,7 +342,7 @@ dependencies = [ [[package]] org = "ballerina" name = "websocket" -version = "2.13.1" +version = "2.13.2" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "constraint"}, From cd92e80f07f832ab28a969cee3ef357a95fd6285 Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 11 Feb 2025 17:08:42 +0530 Subject: [PATCH 03/15] Update licenses and remove parenthesis in if statements --- ballerina/close_frame_return_types.bal | 24 ++++++++-------- ballerina/tests/close_frame_test.bal | 38 +++++++++++++------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/ballerina/close_frame_return_types.bal b/ballerina/close_frame_return_types.bal index e6ea89aec..f2422f294 100644 --- a/ballerina/close_frame_return_types.bal +++ b/ballerina/close_frame_return_types.bal @@ -1,18 +1,18 @@ -// Copyright (c) 2025 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). // -// WSO2 Inc. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. public type Status distinct object { public int code; // Constraint minValue: 1000, maxValue: 4999 diff --git a/ballerina/tests/close_frame_test.bal b/ballerina/tests/close_frame_test.bal index b1445834d..004906f2e 100644 --- a/ballerina/tests/close_frame_test.bal +++ b/ballerina/tests/close_frame_test.bal @@ -1,18 +1,18 @@ -// Copyright (c) 2025 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). // -// WSO2 Inc. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. import ballerina/test; @@ -130,7 +130,7 @@ public function testNormalClosure() returns Error? { check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); - if (res is Error) { + if res is Error { test:assertEquals(res.message(), "Connection closed Status code: 1000"); } } @@ -143,7 +143,7 @@ public function testGoingAway() returns Error? { check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); - if (res is Error) { + if res is Error { test:assertEquals(res.message(), "Connection closed Status code: 1001"); } } @@ -156,7 +156,7 @@ public function testUnsupportedData() returns Error? { check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); - if (res is Error) { + if res is Error { test:assertEquals(res.message(), "Endpoint received unsupported frame: Status code: 1003"); } } @@ -169,7 +169,7 @@ public function testInvalidPayload() returns Error? { check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); - if (res is Error) { + if res is Error { test:assertEquals(res.message(), "Payload does not match the expected format or encoding: Status code: 1007"); } } @@ -182,7 +182,7 @@ public function testPolicyViolation() returns Error? { check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); - if (res is Error) { + if res is Error { test:assertEquals(res.message(), "Received message violates its policy: Status code: 1008"); } } @@ -195,7 +195,7 @@ public function testMessageTooBig() returns Error? { check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); - if (res is Error) { + if res is Error { test:assertEquals(res.message(), "The received message exceeds the allowed size limit: Status code: 1009"); } } @@ -208,7 +208,7 @@ public function testInternalServerError() returns Error? { check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); - if (res is Error) { + if res is Error { test:assertEquals(res.message(), "Internal server error occurred: Status code: 1011"); } } From 02ff641d1c40338ef461a8aa9683d2291b609cde Mon Sep 17 00:00:00 2001 From: "M.C.A. Maddumage" <102851155+chathushkaayash@users.noreply.github.com> Date: Thu, 13 Feb 2025 07:41:55 +0530 Subject: [PATCH 04/15] Sync the fork (#2) * Update chat_application test results on Tue Feb 11 18:29:43 UTC 2025 * Update chat_application test results on Wed Feb 12 18:30:42 UTC 2025 --------- Co-authored-by: ballerina-bot Co-authored-by: ballerina-bot --- load-tests/chat_application/results/summary.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/load-tests/chat_application/results/summary.csv b/load-tests/chat_application/results/summary.csv index 38fcd584f..4838ebac5 100644 --- a/load-tests/chat_application/results/summary.csv +++ b/load-tests/chat_application/results/summary.csv @@ -714,3 +714,5 @@ WebSocket request-response Sampler,10916551,0,1,1,1,4,0,43,0.00%,13995.6,328.0,1 WebSocket request-response Sampler,10189346,0,1,1,1,4,0,45,0.00%,13063.5,306.2,1.52,1731608995,50,10 WebSocket request-response Sampler,10654943,0,1,1,1,4,0,40,0.00%,13660.4,320.2,1.55,1731695302,50,10 WebSocket request-response Sampler,12802567,0,0,1,1,3,0,42,0.00%,16413.9,384.7,1.78,1739212149,50,10 +WebSocket request-response Sampler,11700281,0,0,1,1,3,0,48,0.00%,15000.6,351.6,2.02,1739298535,50,10 +WebSocket request-response Sampler,12585669,0,0,1,1,3,0,44,0.00%,16136.0,378.2,1.79,1739384977,50,10 From 0027a0623201b06d7685cf2dd8053b57ad91147f Mon Sep 17 00:00:00 2001 From: ayash Date: Mon, 17 Feb 2025 09:14:05 +0530 Subject: [PATCH 05/15] Update close frame records based on new suggestions --- ballerina/close_frame_return_types.bal | 118 ++++++------------ ballerina/tests/close_frame_test.bal | 28 +++++ .../stdlib/websocket/WebSocketConstants.java | 4 +- .../WebSocketResourceDispatcher.java | 7 +- 4 files changed, 74 insertions(+), 83 deletions(-) diff --git a/ballerina/close_frame_return_types.bal b/ballerina/close_frame_return_types.bal index f2422f294..72cefb337 100644 --- a/ballerina/close_frame_return_types.bal +++ b/ballerina/close_frame_return_types.bal @@ -14,116 +14,77 @@ // specific language governing permissions and limitations // under the License. -public type Status distinct object { - public int code; // Constraint minValue: 1000, maxValue: 4999 +public readonly distinct class PredefinedCloseFrameType { }; -type CloseFrame record {| - readonly Status status; - string reason?; -|}; - -public const NORMAL_CLOSURE_STATUS_CODE = 1000; -public const GOING_AWAY_STATUS_CODE = 1001; -// public const PROTOCOL_ERROR_STATUS_CODE = 1002; -public const UNSUPPORTED_DATA_STATUS_CODE = 1003; -public const INVALID_PAYLOAD_STATUS_CODE = 1007; -public const POLICY_VIOLATION_STATUS_CODE = 1008; -public const MESSAGE_TOO_BIG_STATUS_CODE = 1009; -public const INTERNAL_SERVER_ERROR_STATUS_CODE = 1011; - -public readonly distinct class NormalClosureStatus { - *Status; - public int code = NORMAL_CLOSURE_STATUS_CODE; -} - -public readonly distinct class GoingAwayStatus { - *Status; - public int code = GOING_AWAY_STATUS_CODE; -} - -// public readonly distinct class ProtocolErrorStatus { -// *Status; -// public int code = PROTOCOL_ERROR_STATUS_CODE; -// } - -public readonly distinct class UnsupportedDataStatus { - *Status; - public int code = UNSUPPORTED_DATA_STATUS_CODE; -} - -public readonly distinct class InvalidPayloadStatus { - *Status; - public int code = INVALID_PAYLOAD_STATUS_CODE; -} - -public readonly distinct class PolicyViolationStatus { - *Status; - public int code = POLICY_VIOLATION_STATUS_CODE; -} +public readonly distinct class CustomCloseFrameType { +}; -public readonly distinct class MessageTooBigStatus { - *Status; - public int code = MESSAGE_TOO_BIG_STATUS_CODE; -} +public final PredefinedCloseFrameType PREDEFINED_CLOSE_FRAME = new; +public final CustomCloseFrameType CUSTOM_CLOSE_FRAME = new; -public readonly distinct class InternalServerErrorStatus { - *Status; - public int code = INTERNAL_SERVER_ERROR_STATUS_CODE; -} +type CloseFrameBase record {| + readonly object {} 'type; + readonly int status; + string reason?; +|}; -public final NormalClosureStatus NORMAL_CLOSURE_STATUS_OBJ = new; -public final GoingAwayStatus GOING_AWAY_STATUS_OBJ = new; -// public final ProtocolErrorStatus PROTOCOL_ERROR_STATUS_OBJ = new; -public final UnsupportedDataStatus UNSUPPORTED_DATA_STATUS_OBJ = new; -public final InvalidPayloadStatus INVALID_PAYLOAD_STATUS_OBJ = new; -public final PolicyViolationStatus POLICY_VIOLATION_STATUS_OBJ = new; -public final MessageTooBigStatus MESSAGE_TOO_BIG_STATUS_OBJ = new; -public final InternalServerErrorStatus INTERNAL_SERVER_ERROR_STATUS_OBJ = new; +public type CustomCloseFrame record {| + *CloseFrameBase; + readonly CustomCloseFrameType 'type = CUSTOM_CLOSE_FRAME; +|}; public type NormalClosure record {| - *CloseFrame; - readonly NormalClosureStatus status = NORMAL_CLOSURE_STATUS_OBJ; + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1000 status = 1000; |}; public type GoingAway record {| - *CloseFrame; - readonly GoingAwayStatus status = GOING_AWAY_STATUS_OBJ; + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1001 status = 1001; |}; // public type ProtocolError record {| -// *CloseFrame; -// readonly ProtocolErrorStatus status = PROTOCOL_ERROR_STATUS_OBJ; +// *CloseFrameBase; +// readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; +// readonly 1002 status = 1002; // string reason = "Connection closed due to protocol error"; // |}; public type UnsupportedData record {| - *CloseFrame; - readonly UnsupportedDataStatus status = UNSUPPORTED_DATA_STATUS_OBJ; + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1003 status = 1003; string reason = "Endpoint received unsupported frame"; |}; public type InvalidPayload record {| - *CloseFrame; - readonly InvalidPayloadStatus status = INVALID_PAYLOAD_STATUS_OBJ; + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1007 status = 1007; string reason = "Payload does not match the expected format or encoding"; |}; public type PolicyViolation record {| - *CloseFrame; - readonly PolicyViolationStatus status = POLICY_VIOLATION_STATUS_OBJ; + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1008 status = 1008; string reason = "Received message violates its policy"; |}; public type MessageTooBig record {| - *CloseFrame; - readonly MessageTooBigStatus status = MESSAGE_TOO_BIG_STATUS_OBJ; + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1009 status = 1009; string reason = "The received message exceeds the allowed size limit"; |}; public type InternalServerError record {| - *CloseFrame; - readonly InternalServerErrorStatus status = INTERNAL_SERVER_ERROR_STATUS_OBJ; + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1011 status = 1011; string reason = "Internal server error occurred"; |}; @@ -135,3 +96,6 @@ public final readonly & InvalidPayload INVALID_PAYLOAD = {}; public final readonly & PolicyViolation POLICY_VIOLATION = {}; public final readonly & MessageTooBig MESSAGE_TOO_BIG = {}; public final readonly & InternalServerError INTERNAL_SERVER_ERROR = {}; + +public type CloseFrame NormalClosure|GoingAway|UnsupportedData|InvalidPayload| + PolicyViolation|MessageTooBig|InternalServerError|CustomCloseFrame; diff --git a/ballerina/tests/close_frame_test.bal b/ballerina/tests/close_frame_test.bal index 004906f2e..af40398b4 100644 --- a/ballerina/tests/close_frame_test.bal +++ b/ballerina/tests/close_frame_test.bal @@ -23,6 +23,7 @@ listener Listener l105 = new (22084); listener Listener l106 = new (22085); listener Listener l107 = new (22086); listener Listener l108 = new (22087); +listener Listener l109 = new (22088); service /onCloseFrame on l102 { resource function get .() returns Service|UpgradeError { @@ -66,6 +67,12 @@ service /onCloseFrame on l108 { } } +service /onCloseFrame on l109 { + resource function get .() returns Service|UpgradeError { + return new WsService109(); + } +} + service class WsService102 { *Service; @@ -122,6 +129,14 @@ service class WsService108 { } } +service class WsService109 { + *Service; + + remote function onMessage(Caller caller, string data) returns CloseFrame { + return {status: 3555, reason: "Custom close frame message"}; + } +} + @test:Config { groups: ["closeFrame"] } @@ -212,3 +227,16 @@ public function testInternalServerError() returns Error? { test:assertEquals(res.message(), "Internal server error occurred: Status code: 1011"); } } + +@test:Config { + groups: ["closeFrame"] +} +public function testCustomCloseFrame() returns Error? { + Client wsClient = check new ("ws://localhost:22088/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if res is Error { + test:assertEquals(res.message(), "Custom close frame message: Status code: 3555"); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index 969f20691..fdf296bc1 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -140,8 +140,8 @@ public class WebSocketConstants { public static final String STATUS_CODE = "Status code:"; // Close Frame Records - public static final BString CLOSE_FRAME_STATUS = StringUtils.fromString("status"); - public static final BString CLOSE_FRAME_STATUS_CODE = StringUtils.fromString("code"); + public static final BString CLOSE_FRAME_TYPE = StringUtils.fromString("type"); + public static final BString CLOSE_FRAME_STATUS_CODE = StringUtils.fromString("status"); public static final BString CLOSE_FRAME_REASON = StringUtils.fromString("reason"); private WebSocketConstants() { diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index b064f5c8e..96a3b02b9 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -1130,8 +1130,7 @@ private static void executeResource(WebSocketService wsService, BObject balservi if (isCloseFrameRecord(result)) { @SuppressWarnings(WebSocketConstants.UNCHECKED) BMap closeFrameRecord = (BMap) result; - long status = ((BObject) closeFrameRecord.get(WebSocketConstants.CLOSE_FRAME_STATUS)) - .getIntValue(WebSocketConstants.CLOSE_FRAME_STATUS_CODE); + long status = closeFrameRecord.getIntValue(WebSocketConstants.CLOSE_FRAME_STATUS_CODE); BString reason = closeFrameRecord.containsKey(WebSocketConstants.CLOSE_FRAME_REASON) ? (BString) closeFrameRecord.get(WebSocketConstants.CLOSE_FRAME_REASON) : StringUtils.fromString(""); @@ -1151,8 +1150,8 @@ private static void executeResource(WebSocketService wsService, BObject balservi private static boolean isCloseFrameRecord(Object obj) { if (obj instanceof BMap) { BMap bMap = (BMap) obj; - return bMap.containsKey(WebSocketConstants.CLOSE_FRAME_STATUS) && - bMap.get(WebSocketConstants.CLOSE_FRAME_STATUS) instanceof BObject; + return bMap.containsKey(WebSocketConstants.CLOSE_FRAME_TYPE) && + bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE) instanceof BObject; } return false; } From 34f51f6b90d423e5530e8456144e055ab63c592f Mon Sep 17 00:00:00 2001 From: ayash Date: Mon, 17 Feb 2025 09:20:15 +0530 Subject: [PATCH 06/15] Fix type casting for close frame reason in WebSocketResourceDispatcher --- .../ballerina/stdlib/websocket/WebSocketResourceDispatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index 96a3b02b9..9fe803c3f 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -1132,7 +1132,7 @@ private static void executeResource(WebSocketService wsService, BObject balservi BMap closeFrameRecord = (BMap) result; long status = closeFrameRecord.getIntValue(WebSocketConstants.CLOSE_FRAME_STATUS_CODE); BString reason = closeFrameRecord.containsKey(WebSocketConstants.CLOSE_FRAME_REASON) ? - (BString) closeFrameRecord.get(WebSocketConstants.CLOSE_FRAME_REASON) + closeFrameRecord.getStringValue(WebSocketConstants.CLOSE_FRAME_REASON) : StringUtils.fromString(""); result = wsService.getRuntime().callMethod( connectionInfo.getWebSocketEndpoint(), WebSocketConstants.RESOURCE_NAME_CLOSE, From 40eb6eac80677796c2b4e59bc721f000593f331e Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 18 Feb 2025 18:30:26 +0530 Subject: [PATCH 07/15] Add `ProtocolError` close frame record and related tests --- ballerina/close_frame_return_types.bal | 16 ++--- ballerina/tests/close_frame_test.bal | 70 +++++++++++++------ .../tests/websocket_client_exceptions.bal | 2 +- ballerina/websocket_errors.bal | 4 +- .../stdlib/websocket/WebSocketConstants.java | 2 +- .../stdlib/websocket/WebSocketUtil.java | 4 +- 6 files changed, 63 insertions(+), 35 deletions(-) diff --git a/ballerina/close_frame_return_types.bal b/ballerina/close_frame_return_types.bal index 72cefb337..7de2fa4a7 100644 --- a/ballerina/close_frame_return_types.bal +++ b/ballerina/close_frame_return_types.bal @@ -46,12 +46,12 @@ public type GoingAway record {| readonly 1001 status = 1001; |}; -// public type ProtocolError record {| -// *CloseFrameBase; -// readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; -// readonly 1002 status = 1002; -// string reason = "Connection closed due to protocol error"; -// |}; +public type ProtocolError record {| + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1002 status = 1002; + string reason = "Connection closed due to protocol error"; +|}; public type UnsupportedData record {| *CloseFrameBase; @@ -90,12 +90,12 @@ public type InternalServerError record {| public final readonly & NormalClosure NORMAL_CLOSURE = {}; public final readonly & GoingAway GOING_AWAY = {}; -// public final readonly & ProtocolError PROTOCOL_ERROR = {}; +public final readonly & ProtocolError PROTOCOL_ERROR = {}; public final readonly & UnsupportedData UNSUPPORTED_DATA = {}; public final readonly & InvalidPayload INVALID_PAYLOAD = {}; public final readonly & PolicyViolation POLICY_VIOLATION = {}; public final readonly & MessageTooBig MESSAGE_TOO_BIG = {}; public final readonly & InternalServerError INTERNAL_SERVER_ERROR = {}; -public type CloseFrame NormalClosure|GoingAway|UnsupportedData|InvalidPayload| +public type CloseFrame NormalClosure|GoingAway|ProtocolError|UnsupportedData|InvalidPayload| PolicyViolation|MessageTooBig|InternalServerError|CustomCloseFrame; diff --git a/ballerina/tests/close_frame_test.bal b/ballerina/tests/close_frame_test.bal index af40398b4..e53f83283 100644 --- a/ballerina/tests/close_frame_test.bal +++ b/ballerina/tests/close_frame_test.bal @@ -16,14 +16,15 @@ import ballerina/test; -listener Listener l102 = new (22081); -listener Listener l103 = new (22082); -listener Listener l104 = new (22083); -listener Listener l105 = new (22084); -listener Listener l106 = new (22085); -listener Listener l107 = new (22086); -listener Listener l108 = new (22087); -listener Listener l109 = new (22088); +listener Listener l102 = new (22082); +listener Listener l103 = new (22083); +listener Listener l104 = new (22084); +listener Listener l105 = new (22085); +listener Listener l106 = new (22086); +listener Listener l107 = new (22087); +listener Listener l108 = new (22088); +listener Listener l109 = new (22089); +listener Listener l110 = new (22090); service /onCloseFrame on l102 { resource function get .() returns Service|UpgradeError { @@ -73,6 +74,12 @@ service /onCloseFrame on l109 { } } +service /onCloseFrame on l110 { + resource function get .() returns Service|UpgradeError { + return new WsService110(); + } +} + service class WsService102 { *Service; @@ -93,7 +100,7 @@ service class WsService104 { *Service; remote function onMessage(Caller caller, string data) returns CloseFrame { - return UNSUPPORTED_DATA; + return PROTOCOL_ERROR; } } @@ -101,7 +108,7 @@ service class WsService105 { *Service; remote function onMessage(Caller caller, string data) returns CloseFrame { - return INVALID_PAYLOAD; + return UNSUPPORTED_DATA; } } @@ -109,7 +116,7 @@ service class WsService106 { *Service; remote function onMessage(Caller caller, string data) returns CloseFrame { - return POLICY_VIOLATION; + return INVALID_PAYLOAD; } } @@ -117,7 +124,7 @@ service class WsService107 { *Service; remote function onMessage(Caller caller, string data) returns CloseFrame { - return MESSAGE_TOO_BIG; + return POLICY_VIOLATION; } } @@ -125,13 +132,21 @@ service class WsService108 { *Service; remote function onMessage(Caller caller, string data) returns CloseFrame { - return INTERNAL_SERVER_ERROR; + return MESSAGE_TOO_BIG; } } service class WsService109 { *Service; + remote function onMessage(Caller caller, string data) returns CloseFrame { + return INTERNAL_SERVER_ERROR; + } +} + +service class WsService110 { + *Service; + remote function onMessage(Caller caller, string data) returns CloseFrame { return {status: 3555, reason: "Custom close frame message"}; } @@ -141,7 +156,7 @@ service class WsService109 { groups: ["closeFrame"] } public function testNormalClosure() returns Error? { - Client wsClient = check new ("ws://localhost:22081/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22082/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); @@ -154,7 +169,7 @@ public function testNormalClosure() returns Error? { groups: ["closeFrame"] } public function testGoingAway() returns Error? { - Client wsClient = check new ("ws://localhost:22082/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22083/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); @@ -163,11 +178,24 @@ public function testGoingAway() returns Error? { } } +@test:Config { + groups: ["closeFrame"] +} +public function testProtocolError() returns Error? { + Client wsClient = check new ("ws://localhost:22084/onCloseFrame"); + check wsClient->writeMessage("Hi"); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if res is Error { + test:assertEquals(res.message(), "Connection closed due to protocol error: Status code: 1002"); + } +} + @test:Config { groups: ["closeFrame"] } public function testUnsupportedData() returns Error? { - Client wsClient = check new ("ws://localhost:22083/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22085/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); @@ -180,7 +208,7 @@ public function testUnsupportedData() returns Error? { groups: ["closeFrame"] } public function testInvalidPayload() returns Error? { - Client wsClient = check new ("ws://localhost:22084/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22086/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); @@ -193,7 +221,7 @@ public function testInvalidPayload() returns Error? { groups: ["closeFrame"] } public function testPolicyViolation() returns Error? { - Client wsClient = check new ("ws://localhost:22085/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22087/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); @@ -206,7 +234,7 @@ public function testPolicyViolation() returns Error? { groups: ["closeFrame"] } public function testMessageTooBig() returns Error? { - Client wsClient = check new ("ws://localhost:22086/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22088/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); @@ -219,7 +247,7 @@ public function testMessageTooBig() returns Error? { groups: ["closeFrame"] } public function testInternalServerError() returns Error? { - Client wsClient = check new ("ws://localhost:22087/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22089/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); @@ -232,7 +260,7 @@ public function testInternalServerError() returns Error? { groups: ["closeFrame"] } public function testCustomCloseFrame() returns Error? { - Client wsClient = check new ("ws://localhost:22088/onCloseFrame"); + Client wsClient = check new ("ws://localhost:22090/onCloseFrame"); check wsClient->writeMessage("Hi"); anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); diff --git a/ballerina/tests/websocket_client_exceptions.bal b/ballerina/tests/websocket_client_exceptions.bal index b56e75116..afa125622 100644 --- a/ballerina/tests/websocket_client_exceptions.bal +++ b/ballerina/tests/websocket_client_exceptions.bal @@ -83,7 +83,7 @@ public function testLongFrameError() returns Error? { runtime:sleep(0.5); var err = wsClientEp->ping(pingData); if err is error { - test:assertEquals(err.message(), "ProtocolError: io.netty.handler.codec.TooLongFrameException: " + + test:assertEquals(err.message(), "CorruptedFrameError: io.netty.handler.codec.TooLongFrameException: " + "invalid payload for PING (payload length must be <= 125, was 148"); } else { test:assertFail("Mismatched output"); diff --git a/ballerina/websocket_errors.bal b/ballerina/websocket_errors.bal index 540ddc37d..c7082a503 100644 --- a/ballerina/websocket_errors.bal +++ b/ballerina/websocket_errors.bal @@ -24,7 +24,7 @@ public type InvalidHandshakeError distinct Error; public type PayloadTooLargeError distinct Error; # Raised when the other side breaks the protocol. -public type ProtocolError distinct Error; +public type CorruptedFrameError distinct Error; # Raised during connection failures. public type ConnectionError distinct Error; @@ -33,7 +33,7 @@ public type ConnectionError distinct Error; public type ConnectionClosureError distinct ConnectionError; # Raised when an out of order/invalid continuation frame is received. -public type InvalidContinuationFrameError distinct ProtocolError; +public type InvalidContinuationFrameError distinct CorruptedFrameError; # Raised when the WebSocket upgrade is not accepted. public type UpgradeError distinct Error; diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index fdf296bc1..08f04a1fe 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -155,7 +155,7 @@ public enum ErrorCode { ConnectionClosureError("ConnectionClosureError"), InvalidHandshakeError("InvalidHandshakeError"), PayloadTooLargeError("PayloadTooLargeError"), - ProtocolError("ProtocolError"), + CorruptedFrameError("CorruptedFrameError"), ConnectionError("ConnectionError"), InvalidContinuationFrameError("InvalidContinuationFrameError"), HandshakeTimedOut("HandshakeTimedOut"), diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketUtil.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketUtil.java index de1be64c8..e6d54aa20 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketUtil.java @@ -263,7 +263,7 @@ public static BError createErrorByType(Throwable throwable) { if (status == WebSocketCloseStatus.MESSAGE_TOO_BIG) { errorCode = WebSocketConstants.ErrorCode.PayloadTooLargeError.errorCode(); } else { - errorCode = WebSocketConstants.ErrorCode.ProtocolError.errorCode(); + errorCode = WebSocketConstants.ErrorCode.CorruptedFrameError.errorCode(); } } else if (throwable instanceof SSLException) { cause = createErrorCause(throwable.getMessage(), WebSocketConstants.ErrorCode.SslError.errorCode(), @@ -281,7 +281,7 @@ public static BError createErrorByType(Throwable throwable) { } else if (throwable instanceof TooLongFrameException) { errorCode = WebSocketConstants.ErrorCode.PayloadTooLargeError.errorCode(); } else if (throwable instanceof CodecException) { - errorCode = WebSocketConstants.ErrorCode.ProtocolError.errorCode(); + errorCode = WebSocketConstants.ErrorCode.CorruptedFrameError.errorCode(); } else if (throwable instanceof WebSocketHandshakeException) { errorCode = WebSocketConstants.ErrorCode.InvalidHandshakeError.errorCode(); } else if (throwable instanceof IOException) { From dc7e9754f4e04ab673032956e1caa5913594b93b Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 18 Feb 2025 18:38:24 +0530 Subject: [PATCH 08/15] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index dd90c575d..44cb70183 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -76,7 +76,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.13.1" +version = "2.13.2" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, From 1db08cb0f1fe3ec76b7706a1efdc2f68ca18c0f2 Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 18 Feb 2025 18:55:45 +0530 Subject: [PATCH 09/15] Add close frame support tests for custom dispatcher methods --- ballerina/tests/close_frame_test.bal | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ballerina/tests/close_frame_test.bal b/ballerina/tests/close_frame_test.bal index e53f83283..bccebead6 100644 --- a/ballerina/tests/close_frame_test.bal +++ b/ballerina/tests/close_frame_test.bal @@ -25,6 +25,7 @@ listener Listener l107 = new (22087); listener Listener l108 = new (22088); listener Listener l109 = new (22089); listener Listener l110 = new (22090); +listener Listener l111 = new (22091); service /onCloseFrame on l102 { resource function get .() returns Service|UpgradeError { @@ -80,6 +81,16 @@ service /onCloseFrame on l110 { } } +@ServiceConfig { + dispatcherKey: "event", + dispatcherStreamId: "id" +} +service /onCloseFrame on l111 { + resource function get .() returns Service|UpgradeError { + return new WsService111(); + } +} + service class WsService102 { *Service; @@ -152,6 +163,14 @@ service class WsService110 { } } +service class WsService111 { + *Service; + + remote function onHeartbeat(Caller caller, string data) returns CloseFrame { + return NORMAL_CLOSURE; + } +} + @test:Config { groups: ["closeFrame"] } @@ -268,3 +287,16 @@ public function testCustomCloseFrame() returns Error? { test:assertEquals(res.message(), "Custom close frame message: Status code: 3555"); } } + +@test:Config { + groups: ["closeFrame"] +} +public function testCustomDispatcher() returns Error? { + Client wsClient = check new ("ws://localhost:22091/onCloseFrame"); + check wsClient->writeMessage({"event": "heartbeat", "id": "1"}); + anydata|Error res = wsClient->readMessage(); + test:assertTrue(res is Error); + if res is Error { + test:assertEquals(res.message(), "Connection closed Status code: 1000"); + } +} From a3b48fef9d9af12dc8866a5090c15067f128c4d6 Mon Sep 17 00:00:00 2001 From: ayash Date: Wed, 19 Feb 2025 15:34:26 +0530 Subject: [PATCH 10/15] Update changelog and spec --- changelog.md | 6 ++++++ docs/spec/spec.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 162e93c7d..028e3c0af 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,12 @@ This file contains all the notable changes done to the Ballerina WebSocket packa The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.13.2] - 2025-02-19 + +### Added + +- [Implement websocket close frame support](https://github.com/ballerina-platform/ballerina-library/issues/7578) + ## [2.13.1] - 2025-02-11 ### Fixed diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 5dea9b243..ec479e6fb 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -36,6 +36,7 @@ The conforming implementation of the specification is released and included in t * [onError](#onerror) * 3.2.2. [Dispatching custom remote methods](#322-dispatching-custom-remote-methods) * [Dispatching custom error remote methods](#Dispatching custom error remote methods) + * 3.2.3. [Return types](#323-return-types) 4. [Client](#4-client) * 4.1. [Client Configurations](#41-client-configurations) * 4.2. [Initialization](#42-initialization) @@ -397,6 +398,47 @@ dispatching error remote function = "onHeartbeatError" 3. If an unmatching message type receives where a matching remote function is not implemented in the WebSocket service by the user, it gets dispatched to the default `onMessage` remote function if it is implemented. Or else it will get ignored. +#### 3.2.3. [Return types](#323-return-types) + +The resource method supports `records`, `string`, `int`, `boolean`, `decimal`, `float` ,`json`, `xml` and `websocket:CloseFrame` as return types. +Whenever user returns a particular output, that will result in an websocket response to the caller who initiated the call. Therefore, user does not necessarily depend on the `websocket:Caller` and its remote methods to proceed with the response. + +```ballerina +remote isolated function onMessage(string data) returns User|string|int|boolean|decimal|float|json|xml|websocket:CloseFrame { +} +``` + +##### 3.2.3.1. Close Frame Records + +The `CloseFrame` Records represent WebSocket close frames. When a service returns a close frame record, the WebSocket module will automatically send the corresponding close frame and terminate the connection. + +Following is the `websocket:NormalClosure` definition. Likewise, some predefined close frame records are provided. + +```ballerina +public type NormalClosure record {| + *CloseFrameBase; + readonly PredefinedCloseFrameType 'type = PREDEFINED_CLOSE_FRAME; + readonly 1000 status = 1000; +|}; + +remote isolated function onMessage(string data) returns websocket:CloseFrame { + websocket:NormalClosure normalClosure = {status: 1000, reason: "Normal Closure"}; + return normalClosure; +} +``` + +Custom Close Frame records enable users to define close frames with any status code and reason within the range of 1000–4999. + +```ballerina +public type InvalidUserCloseFrame record {| + *websocket:CustomCloseFrame; +|}; + +remote isolated function onMessage(string data) returns InvalidUserCloseFrame { + return {status: 4444, reason: "Invalid User"}; +} +``` + ## 4. [Client](#4-client) `websocket:Client` can be used to send and receive data synchronously over WebSocket connection. The underlying implementation is non-blocking. @@ -760,4 +802,4 @@ public function main() returns error? { string stringResp = check 'string:fromBytes(byteResp); io:println(stringResp); } -``` \ No newline at end of file +``` From f8f177ecabd5ee809a8d65cce98e7929d6570980 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 20 Feb 2025 09:51:21 +0530 Subject: [PATCH 11/15] Update changelog and spec addressing minor issues --- changelog.md | 2 +- docs/spec/spec.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 028e3c0af..64ae0fd98 100644 --- a/changelog.md +++ b/changelog.md @@ -3,7 +3,7 @@ This file contains all the notable changes done to the Ballerina WebSocket packa The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.13.2] - 2025-02-19 +## [Unreleased] ### Added diff --git a/docs/spec/spec.md b/docs/spec/spec.md index ec479e6fb..904baa99b 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -398,7 +398,7 @@ dispatching error remote function = "onHeartbeatError" 3. If an unmatching message type receives where a matching remote function is not implemented in the WebSocket service by the user, it gets dispatched to the default `onMessage` remote function if it is implemented. Or else it will get ignored. -#### 3.2.3. [Return types](#323-return-types) +#### 3.2.3. Return types The resource method supports `records`, `string`, `int`, `boolean`, `decimal`, `float` ,`json`, `xml` and `websocket:CloseFrame` as return types. Whenever user returns a particular output, that will result in an websocket response to the caller who initiated the call. Therefore, user does not necessarily depend on the `websocket:Caller` and its remote methods to proceed with the response. @@ -422,7 +422,7 @@ public type NormalClosure record {| |}; remote isolated function onMessage(string data) returns websocket:CloseFrame { - websocket:NormalClosure normalClosure = {status: 1000, reason: "Normal Closure"}; + websocket:NormalClosure normalClosure = {reason: "Normal Closure"}; return normalClosure; } ``` From e66a2379389ef59b94296e0a08c2d44fce56bb90 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 20 Feb 2025 23:41:53 +0530 Subject: [PATCH 12/15] Add getErrorMessage function for getting error messages from predefined CloseFrames --- ballerina/tests/close_frame_test.bal | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/ballerina/tests/close_frame_test.bal b/ballerina/tests/close_frame_test.bal index bccebead6..c171b2277 100644 --- a/ballerina/tests/close_frame_test.bal +++ b/ballerina/tests/close_frame_test.bal @@ -171,6 +171,14 @@ service class WsService111 { } } +public function getErrorMessage(CloseFrame closeFrame) returns string { + string? reason = closeFrame.reason; + if reason is string { + return reason + ": Status code: " + closeFrame.status.toString(); + } + return "Connection closed Status code: " + closeFrame.status.toString(); +} + @test:Config { groups: ["closeFrame"] } @@ -180,7 +188,7 @@ public function testNormalClosure() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Connection closed Status code: 1000"); + test:assertEquals(res.message(), getErrorMessage(NORMAL_CLOSURE)); } } @@ -193,7 +201,7 @@ public function testGoingAway() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Connection closed Status code: 1001"); + test:assertEquals(res.message(), getErrorMessage(GOING_AWAY)); } } @@ -206,7 +214,7 @@ public function testProtocolError() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Connection closed due to protocol error: Status code: 1002"); + test:assertEquals(res.message(), getErrorMessage(PROTOCOL_ERROR)); } } @@ -219,7 +227,7 @@ public function testUnsupportedData() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Endpoint received unsupported frame: Status code: 1003"); + test:assertEquals(res.message(), getErrorMessage(UNSUPPORTED_DATA)); } } @@ -232,7 +240,7 @@ public function testInvalidPayload() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Payload does not match the expected format or encoding: Status code: 1007"); + test:assertEquals(res.message(), getErrorMessage(INVALID_PAYLOAD)); } } @@ -245,7 +253,7 @@ public function testPolicyViolation() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Received message violates its policy: Status code: 1008"); + test:assertEquals(res.message(), getErrorMessage(POLICY_VIOLATION)); } } @@ -258,7 +266,7 @@ public function testMessageTooBig() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "The received message exceeds the allowed size limit: Status code: 1009"); + test:assertEquals(res.message(), getErrorMessage(MESSAGE_TOO_BIG)); } } @@ -271,7 +279,7 @@ public function testInternalServerError() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Internal server error occurred: Status code: 1011"); + test:assertEquals(res.message(), getErrorMessage(INTERNAL_SERVER_ERROR)); } } @@ -297,6 +305,6 @@ public function testCustomDispatcher() returns Error? { anydata|Error res = wsClient->readMessage(); test:assertTrue(res is Error); if res is Error { - test:assertEquals(res.message(), "Connection closed Status code: 1000"); + test:assertEquals(res.message(), getErrorMessage(NORMAL_CLOSURE)); } } From 6d0b62cfc2f85868a6435dd0758e52105ed6154c Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 21 Feb 2025 00:37:14 +0530 Subject: [PATCH 13/15] Update isCloseFrameRecord logic --- .../io/ballerina/stdlib/websocket/WebSocketConstants.java | 2 ++ .../stdlib/websocket/WebSocketResourceDispatcher.java | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index 08f04a1fe..61b1eaad0 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -143,6 +143,8 @@ public class WebSocketConstants { public static final BString CLOSE_FRAME_TYPE = StringUtils.fromString("type"); public static final BString CLOSE_FRAME_STATUS_CODE = StringUtils.fromString("status"); public static final BString CLOSE_FRAME_REASON = StringUtils.fromString("reason"); + public static final String PREDEFINED_CLOSE_FRAME_TYPE = "websocket:PredefinedCloseFrameType"; + public static final String CUSTOM_CLOSE_FRAME_TYPE = "websocket:CustomCloseFrameType"; private WebSocketConstants() { } diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index 9fe803c3f..4c4c40e70 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -1150,8 +1150,12 @@ private static void executeResource(WebSocketService wsService, BObject balservi private static boolean isCloseFrameRecord(Object obj) { if (obj instanceof BMap) { BMap bMap = (BMap) obj; - return bMap.containsKey(WebSocketConstants.CLOSE_FRAME_TYPE) && - bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE) instanceof BObject; + if (bMap.containsKey(WebSocketConstants.CLOSE_FRAME_STATUS_CODE)) { + String objectType = ((BObject) bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE)) + .getOriginalType().toString(); + return objectType.equals(WebSocketConstants.PREDEFINED_CLOSE_FRAME_TYPE) || + objectType.equals(WebSocketConstants.CUSTOM_CLOSE_FRAME_TYPE); + } } return false; } From fb843cbcfd3ceb71ea0eb30e230d7d42ac27b5b4 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 21 Feb 2025 09:42:18 +0530 Subject: [PATCH 14/15] Fix isCloseFrameRecord checking logic --- .../io/ballerina/stdlib/websocket/WebSocketConstants.java | 4 ++-- .../stdlib/websocket/WebSocketResourceDispatcher.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index 61b1eaad0..793a853a4 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -143,8 +143,8 @@ public class WebSocketConstants { public static final BString CLOSE_FRAME_TYPE = StringUtils.fromString("type"); public static final BString CLOSE_FRAME_STATUS_CODE = StringUtils.fromString("status"); public static final BString CLOSE_FRAME_REASON = StringUtils.fromString("reason"); - public static final String PREDEFINED_CLOSE_FRAME_TYPE = "websocket:PredefinedCloseFrameType"; - public static final String CUSTOM_CLOSE_FRAME_TYPE = "websocket:CustomCloseFrameType"; + public static final String PREDEFINED_CLOSE_FRAME_TYPE = PACKAGE_WEBSOCKET + COLON + "PredefinedCloseFrameType"; + public static final String CUSTOM_CLOSE_FRAME_TYPE = PACKAGE_WEBSOCKET + COLON + "CustomCloseFrameType"; private WebSocketConstants() { } diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index 4c4c40e70..06602b147 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -1150,7 +1150,8 @@ private static void executeResource(WebSocketService wsService, BObject balservi private static boolean isCloseFrameRecord(Object obj) { if (obj instanceof BMap) { BMap bMap = (BMap) obj; - if (bMap.containsKey(WebSocketConstants.CLOSE_FRAME_STATUS_CODE)) { + if (bMap.containsKey(WebSocketConstants.CLOSE_FRAME_TYPE) && + bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE) instanceof BObject) { String objectType = ((BObject) bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE)) .getOriginalType().toString(); return objectType.equals(WebSocketConstants.PREDEFINED_CLOSE_FRAME_TYPE) || From 0e8859267df837dafff72e5e4d6a85d36183b38a Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 21 Feb 2025 21:03:48 +0530 Subject: [PATCH 15/15] Update close frame type checking to use TypeUtils --- .../stdlib/websocket/WebSocketResourceDispatcher.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index 06602b147..ba5f2d1fc 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -1152,8 +1152,7 @@ private static boolean isCloseFrameRecord(Object obj) { BMap bMap = (BMap) obj; if (bMap.containsKey(WebSocketConstants.CLOSE_FRAME_TYPE) && bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE) instanceof BObject) { - String objectType = ((BObject) bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE)) - .getOriginalType().toString(); + String objectType = TypeUtils.getType(bMap.get(WebSocketConstants.CLOSE_FRAME_TYPE)).toString(); return objectType.equals(WebSocketConstants.PREDEFINED_CLOSE_FRAME_TYPE) || objectType.equals(WebSocketConstants.CUSTOM_CLOSE_FRAME_TYPE); }