๐Ÿ‘ a fluffy Gleam web server
23

Configure Feed

Select the types of activity you want to include in your feed.

Add API for sending close frame with an error code status (#10)

authored by

Vladislav Shakitskiy and committed by
GitHub
(Nov 9, 2025, 3:14 PM +0300) 24c6d366 56847669

+121 -1
+5
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + # Unreleased 4 + 5 + - Add `send_close_frame` function for the WebSocket API 6 + - Add `CloseCode` for various close frames 7 + 3 8 # v2.0.3 - 06.11.2025 4 9 5 10 - Replace `gramps` with `websocks` package
+1 -1
manifest.toml
··· 21 21 22 22 [requirements] 23 23 compresso = { version = "0.1.0" } 24 + gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" } 24 25 gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } 25 26 gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 26 27 gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" } ··· 31 32 glisten = { version = ">= 8.0.1 and < 9.0.0" } 32 33 logging = { version = ">= 1.3.0 and < 2.0.0" } 33 34 websocks = { version = ">= 2.0.0 and < 3.0.0" } 34 - gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" }
+87
src/ewe.gleam
··· 62 62 //// "upgrade_websocket", 63 63 //// "send_binary_frame", 64 64 //// "send_text_frame", 65 + //// "send_close_frame", 65 66 //// "websocket_continue", 66 67 //// "websocket_continue_with_selector", 67 68 //// "websocket_stop", ··· 864 865 WebsocketAbnormalStop(reason) 865 866 } 866 867 868 + fn to_websocket_next( 869 + next: websocket.WebsocketNext(user_state, user_message), 870 + ) -> WebsocketNext(user_state, user_message) { 871 + case next { 872 + websocket.Continue(user_state, selector) -> 873 + WebsocketContinue(user_state, selector) 874 + websocket.NormalStop -> WebsocketNormalStop 875 + websocket.AbnormalStop(reason) -> WebsocketAbnormalStop(reason) 876 + } 877 + } 878 + 867 879 fn to_internal_websocket_next( 868 880 next: WebsocketNext(user_state, user_message), 869 881 ) -> websocket.WebsocketNext(user_state, user_message) { ··· 994 1006 conn.context, 995 1007 bit_array.from_string(text), 996 1008 ) 1009 + } 1010 + 1011 + /// WebSocket close codes that can be sent when closing a connection. The `data` 1012 + /// parameter allows you to include payload up to 123 bytes in size. 1013 + /// 1014 + pub type CloseCode { 1015 + /// Standard graceful shutdown (1000). Use when connection completed 1016 + /// successfully. 1017 + /// 1018 + NormalClosure(data: String) 1019 + /// Invalid message format (1007). Received payload that doesn't match what 1020 + /// you expected. 1021 + /// 1022 + InvalidPayloadData(data: String) 1023 + /// Application policy violation (1008).Client broke your rules - failed 1024 + /// authentication, hit rate limits, or violated business logic. 1025 + /// 1026 + PolicyViolation(data: String) 1027 + /// Message exceeds size limits (1009). Client sent something bigger than 1028 + /// your application allows. 1029 + /// 1030 + MessageTooBig(data: String) 1031 + /// Server encountered unexpected error (1011). Something went wrong on your 1032 + /// side that prevents handling the connection. 1033 + /// 1034 + InternalError(data: String) 1035 + /// Server is restarting (1012). Planned restart - clients can reconnect 1036 + /// after a bit. 1037 + /// 1038 + ServiceRestart(data: String) 1039 + /// Temporary server overload (1013). Use when server is temporarily 1040 + /// unavailable, client should retry. 1041 + /// 1042 + TryAgainLater(data: String) 1043 + /// Gateway/proxy received invalid response (1014). You're acting as a proxy 1044 + /// and the upstream server gave you garbage. 1045 + /// 1046 + BadGateway(data: String) 1047 + /// Custom close codes 3000-4999 for application-specific use. 1048 + /// 1049 + CustomCloseCode(code: Int, data: String) 1050 + /// Close without a specific reason. 1051 + /// 1052 + NoCloseReason 1053 + } 1054 + 1055 + fn to_internal_close_code(code: CloseCode) -> websocks.CloseReason { 1056 + case code { 1057 + NormalClosure(data) -> websocks.NormalClosure(bit_array.from_string(data)) 1058 + InvalidPayloadData(data) -> 1059 + websocks.InvalidPayloadData(bit_array.from_string(data)) 1060 + PolicyViolation(data) -> 1061 + websocks.PolicyViolation(bit_array.from_string(data)) 1062 + MessageTooBig(data) -> websocks.MessageTooBig(bit_array.from_string(data)) 1063 + InternalError(data) -> websocks.InternalError(bit_array.from_string(data)) 1064 + ServiceRestart(data) -> websocks.ServiceRestart(bit_array.from_string(data)) 1065 + TryAgainLater(data) -> websocks.TryAgainLater(bit_array.from_string(data)) 1066 + BadGateway(data) -> websocks.BadGateway(bit_array.from_string(data)) 1067 + CustomCloseCode(code, data) -> 1068 + websocks.CustomCloseCode(code, bit_array.from_string(data)) 1069 + NoCloseReason -> websocks.NoCloseReason 1070 + } 1071 + } 1072 + 1073 + /// Sends a close frame to the websocket client. Once this function is called, 1074 + /// no other frames can be sent on this connection. Returns how the WebSocket 1075 + /// connection should proceed - make sure your handler returns this value. 1076 + /// 1077 + pub fn send_close_frame( 1078 + conn: WebsocketConnection, 1079 + code: CloseCode, 1080 + ) -> WebsocketNext(user_state, user_message) { 1081 + to_internal_close_code(code) 1082 + |> websocket.send_close_frame(conn.transport, conn.socket, _) 1083 + |> to_websocket_next() 997 1084 } 998 1085 999 1086 // -----------------------------------------------------------------------------
+28
src/ewe/internal/stream/websocket.gleam
··· 259 259 } 260 260 } 261 261 262 + pub fn send_close_frame( 263 + transport: Transport, 264 + socket: Socket, 265 + code: websocks.CloseReason, 266 + ) -> WebsocketNext(user_state, user_message) { 267 + let frame = 268 + exception.rescue(fn() { 269 + websocks.encode_close_frame(code, None) 270 + |> bytes_tree.from_bit_array() 271 + |> transport.send(transport, socket, _) 272 + }) 273 + 274 + case frame { 275 + Ok(Ok(Nil)) -> NormalStop 276 + Ok(Error(reason)) -> 277 + AbnormalStop("Failed to send close frame: " <> string.inspect(reason)) 278 + Error(reason) -> { 279 + logging.log( 280 + logging.Error, 281 + "Frame should be sent from the WebSocket connection, but was sent from different process: " 282 + <> string.inspect(reason), 283 + ) 284 + 285 + panic as non_owning_process 286 + } 287 + } 288 + } 289 + 262 290 // ----------------------------------------------------------------------------- 263 291 // MESSAGE HANDLING 264 292 // -----------------------------------------------------------------------------