From 764e9c4a56f0535cc23c1fa5eecb520faaeed91b Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Fri, 25 Jul 2025 16:44:15 +0200 Subject: [PATCH 1/7] update close reason type with custom close codes --- src/stratus.gleam | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/stratus.gleam b/src/stratus.gleam index ae14b60..6802c2a 100644 --- a/src/stratus.gleam +++ b/src/stratus.gleam @@ -684,6 +684,13 @@ pub type CloseReason { MessageTooBig(body: BitArray) MissingExtensions(body: BitArray) UnexpectedCondition(body: BitArray) + /// Usually used for `4000` codes. + CustomCloseReason( + /// If `code > 5000`, then it will be treated like a `Normal` close reason. + /// See https://hexdocs.pm/gramps/gramps/websocket.html#CloseReason. + code: Int, + body: BitArray, + ) } fn convert_close_reason(reason: CloseReason) -> websocket.CloseReason { @@ -697,6 +704,7 @@ fn convert_close_reason(reason: CloseReason) -> websocket.CloseReason { ProtocolError(body:) -> websocket.ProtocolError(body:) UnexpectedCondition(body:) -> websocket.UnexpectedCondition(body:) UnexpectedDataType(body:) -> websocket.UnexpectedDataType(body:) + CustomCloseReason(code:, body:) -> websocket.CustomCloseReason(code:, body:) } } From 19a264454015071727345ad0b837e6e0b24d73b0 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Sun, 10 Aug 2025 00:27:10 +0200 Subject: [PATCH 2/7] make CloseReason type opaque --- src/stratus.gleam | 79 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/src/stratus.gleam b/src/stratus.gleam index 6802c2a..de4c88f 100644 --- a/src/stratus.gleam +++ b/src/stratus.gleam @@ -669,12 +669,8 @@ pub fn send_ping(conn: Connection, data: BitArray) -> Result(Nil, SocketReason) |> result.map_error(convert_socket_reason) } -/// This will close the WebSocket connection. -pub fn close(conn: Connection) -> Result(Nil, SocketReason) { - close_with_reason(conn, Normal(body: <<>>)) -} - -pub type CloseReason { +pub opaque type CloseReason { + NotProvided Normal(body: BitArray) GoingAway(body: BitArray) ProtocolError(body: BitArray) @@ -684,17 +680,12 @@ pub type CloseReason { MessageTooBig(body: BitArray) MissingExtensions(body: BitArray) UnexpectedCondition(body: BitArray) - /// Usually used for `4000` codes. - CustomCloseReason( - /// If `code > 5000`, then it will be treated like a `Normal` close reason. - /// See https://hexdocs.pm/gramps/gramps/websocket.html#CloseReason. - code: Int, - body: BitArray, - ) + CustomCloseReason(code: Int, body: BitArray) } fn convert_close_reason(reason: CloseReason) -> websocket.CloseReason { case reason { + NotProvided -> websocket.NotProvided GoingAway(body:) -> websocket.GoingAway(body:) InconsistentDataType(body:) -> websocket.InconsistentDataType(body:) MessageTooBig(body:) -> websocket.MessageTooBig(body:) @@ -708,7 +699,67 @@ fn convert_close_reason(reason: CloseReason) -> websocket.CloseReason { } } -/// This closes the WebSocket connection with a particular close reason. +/// Closes without a reason. +pub fn close(conn: Connection) { + close_with_reason(conn, NotProvided) +} + +/// Status code: 1000 +pub fn close_reason_normal(body: BitArray) -> CloseReason { + Normal(body:) +} + +/// Status code: 1001 +pub fn close_reason_going_away(body: BitArray) -> CloseReason { + GoingAway(body:) +} + +/// Status code: 1002 +pub fn close_reason_protocol_error(body: BitArray) -> CloseReason { + ProtocolError(body:) +} + +/// Status code: 1003 +pub fn close_reason_unexpected_data_type(body: BitArray) -> CloseReason { + UnexpectedDataType(body:) +} + +/// Status code: 1007 +pub fn close_reason_inconsistent_data_type(body: BitArray) -> CloseReason { + InconsistentDataType(body:) +} + +/// Status code: 1008 +pub fn close_reason_policy_violation(body: BitArray) -> CloseReason { + PolicyViolation(body:) +} + +/// Status code: 1009 +pub fn close_reason_message_too_big(body: BitArray) -> CloseReason { + MessageTooBig(body:) +} + +/// Status code: 1010 +pub fn close_reason_missing_extensions(body: BitArray) -> CloseReason { + MissingExtensions(body:) +} + +/// Status code: 1011 +pub fn close_reason_unexpected_condition(body: BitArray) -> CloseReason { + UnexpectedCondition(body:) +} + +/// Accepts codes from 0 to 4999. +pub fn close_reason_custom( + code: Int, + body: BitArray, +) -> Result(CloseReason, Nil) { + case code >= 5000 { + True -> Error(Nil) + False -> Ok(CustomCloseReason(code:, body:)) + } +} + pub fn close_with_reason( conn: Connection, reason: CloseReason, From a073bd5a0d9d80b6f61e57e2050957c36d86bcd6 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Sun, 10 Aug 2025 00:57:21 +0200 Subject: [PATCH 3/7] adjust README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3cc05e..d3c5d14 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ pub fn main() { stratus.Binary(_msg) -> stratus.continue(state) stratus.User(Close) -> { let assert Ok(_) = - stratus.close_with_reason(conn, stratus.GoingAway(<<"goodbye">>)) + stratus.close_with_reason(conn, stratus.close_reason_going_away(<<"goodbye">>)) stratus.stop() } } From be3f4eff665bc1abd30347c9b172cdc9d3cc1cf0 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Thu, 21 Aug 2025 03:36:30 +0200 Subject: [PATCH 4/7] don't require piping into close_with_reason Signed-off-by: Filip Hoffmann --- src/stratus.gleam | 59 ++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/stratus.gleam b/src/stratus.gleam index de4c88f..fdb5500 100644 --- a/src/stratus.gleam +++ b/src/stratus.gleam @@ -1,5 +1,6 @@ import exception import gleam/bit_array +import gleam/bool import gleam/bytes_tree.{type BytesTree} import gleam/crypto import gleam/erlang/charlist @@ -72,6 +73,11 @@ pub type SocketReason { Exbadseq } +pub type CustomCloseError { + SocketFail(SocketReason) + InvalidCode +} + fn convert_socket_reason(reason: socket.SocketReason) -> SocketReason { case reason { socket.Badarg -> Badarg @@ -267,7 +273,7 @@ pub type InitializationError { HandshakeFailed(HandshakeError) // The actor failed to start, most likely due to a timeout in your `init` ActorFailed(actor.StartError) - // + // FailedToTransferSocket(SocketReason) } @@ -705,62 +711,63 @@ pub fn close(conn: Connection) { } /// Status code: 1000 -pub fn close_reason_normal(body: BitArray) -> CloseReason { - Normal(body:) +pub fn close_normal(conn: Connection, body: BitArray) { + close_with_reason(conn, Normal(body:)) } /// Status code: 1001 -pub fn close_reason_going_away(body: BitArray) -> CloseReason { - GoingAway(body:) +pub fn close_going_away(conn: Connection, body: BitArray) { + close_with_reason(conn, GoingAway(body:)) } /// Status code: 1002 -pub fn close_reason_protocol_error(body: BitArray) -> CloseReason { - ProtocolError(body:) +pub fn close_protocol_error(conn: Connection, body: BitArray) { + close_with_reason(conn, ProtocolError(body:)) } /// Status code: 1003 -pub fn close_reason_unexpected_data_type(body: BitArray) -> CloseReason { - UnexpectedDataType(body:) +pub fn close_unexpected_data_type(conn: Connection, body: BitArray) { + close_with_reason(conn, UnexpectedDataType(body:)) } /// Status code: 1007 -pub fn close_reason_inconsistent_data_type(body: BitArray) -> CloseReason { - InconsistentDataType(body:) +pub fn close_inconsistent_data_type(conn: Connection, body: BitArray) { + close_with_reason(conn, InconsistentDataType(body:)) } /// Status code: 1008 -pub fn close_reason_policy_violation(body: BitArray) -> CloseReason { - PolicyViolation(body:) +pub fn close_policy_violation(conn: Connection, body: BitArray) { + close_with_reason(conn, PolicyViolation(body:)) } /// Status code: 1009 -pub fn close_reason_message_too_big(body: BitArray) -> CloseReason { - MessageTooBig(body:) +pub fn close_message_too_big(conn: Connection, body: BitArray) { + close_with_reason(conn, MessageTooBig(body:)) } /// Status code: 1010 -pub fn close_reason_missing_extensions(body: BitArray) -> CloseReason { - MissingExtensions(body:) +pub fn close_missing_extensions(conn: Connection, body: BitArray) { + close_with_reason(conn, MissingExtensions(body:)) } /// Status code: 1011 -pub fn close_reason_unexpected_condition(body: BitArray) -> CloseReason { - UnexpectedCondition(body:) +pub fn close_unexpected_condition(conn: Connection, body: BitArray) { + close_with_reason(conn, UnexpectedCondition(body:)) } /// Accepts codes from 0 to 4999. -pub fn close_reason_custom( +pub fn close_custom( + conn: Connection, code: Int, body: BitArray, -) -> Result(CloseReason, Nil) { - case code >= 5000 { - True -> Error(Nil) - False -> Ok(CustomCloseReason(code:, body:)) - } +) -> Result(Nil, CustomCloseError) { + use <- bool.guard(when: code >= 5000, return: Error(InvalidCode)) + + close_with_reason(conn, CustomCloseReason(code:, body:)) + |> result.map_error(SocketFail) } -pub fn close_with_reason( +fn close_with_reason( conn: Connection, reason: CloseReason, ) -> Result(Nil, SocketReason) { From 9749172b5b6f611c2dd0d5ee01c2bb5eb5c2edc7 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Thu, 21 Aug 2025 03:47:13 +0200 Subject: [PATCH 5/7] Update README.md Signed-off-by: Filip Hoffmann --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d3c5d14..bc3dfe5 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ pub fn main() { stratus.Binary(_msg) -> stratus.continue(state) stratus.User(Close) -> { let assert Ok(_) = - stratus.close_with_reason(conn, stratus.close_reason_going_away(<<"goodbye">>)) + stratus.close_going_away(conn, <<"goodbye!">>) stratus.stop() } } From 5e9acf7910d42b11be0a16535c3285cf4155f85b Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Sun, 7 Sep 2025 17:10:29 +0200 Subject: [PATCH 6/7] make only CustomCloseReason opaque --- src/stratus.gleam | 87 +++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 59 deletions(-) diff --git a/src/stratus.gleam b/src/stratus.gleam index fdb5500..63f1e84 100644 --- a/src/stratus.gleam +++ b/src/stratus.gleam @@ -675,17 +675,32 @@ pub fn send_ping(conn: Connection, data: BitArray) -> Result(Nil, SocketReason) |> result.map_error(convert_socket_reason) } -pub opaque type CloseReason { +pub type CloseReason { NotProvided + /// Status code: 1000 Normal(body: BitArray) + /// Status code: 1001 GoingAway(body: BitArray) + /// Status code: 1002 ProtocolError(body: BitArray) + /// Status code: 1003 UnexpectedDataType(body: BitArray) + /// Status code: 1007 InconsistentDataType(body: BitArray) + /// Status code: 1008 PolicyViolation(body: BitArray) + /// Status code: 1009 MessageTooBig(body: BitArray) + /// Status code: 1010 MissingExtensions(body: BitArray) + /// Status code: 1011 UnexpectedCondition(body: BitArray) + /// Use [`close_custom`](#close_custom) to send a custom close code. + Custom(CustomCloseReason) +} + +/// Use [`close_custom`](#close_custom) to send a custom close code. +pub opaque type CustomCloseReason { CustomCloseReason(code: Int, body: BitArray) } @@ -701,75 +716,29 @@ fn convert_close_reason(reason: CloseReason) -> websocket.CloseReason { ProtocolError(body:) -> websocket.ProtocolError(body:) UnexpectedCondition(body:) -> websocket.UnexpectedCondition(body:) UnexpectedDataType(body:) -> websocket.UnexpectedDataType(body:) - CustomCloseReason(code:, body:) -> websocket.CustomCloseReason(code:, body:) + Custom(CustomCloseReason(code:, body:)) -> + websocket.CustomCloseReason(code:, body:) } } -/// Closes without a reason. -pub fn close(conn: Connection) { - close_with_reason(conn, NotProvided) -} - -/// Status code: 1000 -pub fn close_normal(conn: Connection, body: BitArray) { - close_with_reason(conn, Normal(body:)) -} - -/// Status code: 1001 -pub fn close_going_away(conn: Connection, body: BitArray) { - close_with_reason(conn, GoingAway(body:)) -} - -/// Status code: 1002 -pub fn close_protocol_error(conn: Connection, body: BitArray) { - close_with_reason(conn, ProtocolError(body:)) -} - -/// Status code: 1003 -pub fn close_unexpected_data_type(conn: Connection, body: BitArray) { - close_with_reason(conn, UnexpectedDataType(body:)) -} - -/// Status code: 1007 -pub fn close_inconsistent_data_type(conn: Connection, body: BitArray) { - close_with_reason(conn, InconsistentDataType(body:)) -} - -/// Status code: 1008 -pub fn close_policy_violation(conn: Connection, body: BitArray) { - close_with_reason(conn, PolicyViolation(body:)) -} - -/// Status code: 1009 -pub fn close_message_too_big(conn: Connection, body: BitArray) { - close_with_reason(conn, MessageTooBig(body:)) -} - -/// Status code: 1010 -pub fn close_missing_extensions(conn: Connection, body: BitArray) { - close_with_reason(conn, MissingExtensions(body:)) -} - -/// Status code: 1011 -pub fn close_unexpected_condition(conn: Connection, body: BitArray) { - close_with_reason(conn, UnexpectedCondition(body:)) -} - -/// Accepts codes from 0 to 4999. +/// Closes the connection with a custom close code between 1000 and 4999. pub fn close_custom( conn: Connection, - code: Int, - body: BitArray, + code code: Int, + body body: BitArray, ) -> Result(Nil, CustomCloseError) { - use <- bool.guard(when: code >= 5000, return: Error(InvalidCode)) + use <- bool.guard( + when: code >= 5000 || code < 1000, + return: Error(InvalidCode), + ) - close_with_reason(conn, CustomCloseReason(code:, body:)) + close(conn, Custom(CustomCloseReason(code:, body:))) |> result.map_error(SocketFail) } -fn close_with_reason( +pub fn close( conn: Connection, - reason: CloseReason, + because reason: CloseReason, ) -> Result(Nil, SocketReason) { let reason = convert_close_reason(reason) let mask = crypto.strong_random_bytes(4) From 3744abe180fc973c077958fe61bd4a8ccdda9bb2 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Sun, 7 Sep 2025 17:37:31 +0200 Subject: [PATCH 7/7] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc3dfe5..120a858 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ pub fn main() { stratus.Binary(_msg) -> stratus.continue(state) stratus.User(Close) -> { let assert Ok(_) = - stratus.close_going_away(conn, <<"goodbye!">>) + stratus.close(conn, stratus.GoingAway(<<"goodbye!">>)) stratus.stop() } }