๐Ÿ‘ a fluffy Gleam web server
23

Configure Feed

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

general changes, playing around

vshakitskiy (Jan 19, 2026, 11:54 AM +0300) c973238f b62236be

+173 -131
+2
CHANGELOG.md
··· 2 2 3 3 # Unreleased 4 4 5 + - Bump dependency package versions 5 6 - Move examples to appropriate folder 6 7 - Improve examples with useful documentation lines 8 + - Replace active socket tcp message decoding for WebSocket selector 7 9 8 10 # v2.1.2 - 19.11.2025 9 11
+4 -1
Makefile
··· 16 16 -v "${PWD}/autobahn:/reports" \ 17 17 --network host \ 18 18 crossbario/autobahn-testsuite \ 19 - wstest -m fuzzingclient -s /autobahn.json 19 + wstest -m fuzzingclient -s /autobahn.json 20 + 21 + autobahn_serve: 22 + cd "${PWD}/autobahn" && bun run serve.ts
+2 -4
autobahn/config.json
··· 6 6 "agent": "ewe" 7 7 } 8 8 ], 9 - "cases": [ 10 - "*" 11 - ], 9 + "cases": ["*"], 12 10 "exclude-cases": [], 13 11 "exclude-agent-cases": {}, 14 12 "optioms": { 15 13 "failByDrop": false 16 14 } 17 - } 15 + }
+19
autobahn/serve.ts
··· 1 + const server = Bun.serve({ 2 + async fetch(req) { 3 + const url = new URL(req.url); 4 + const filePath = `./server${url.pathname}`; 5 + 6 + const file = Bun.file(filePath); 7 + const exists = await file.exists(); 8 + if (exists) { 9 + return new Response(file); 10 + } 11 + 12 + return new Response("Not Found", { status: 404 }); 13 + }, 14 + port: 3000, 15 + hostname: "0.0.0.0", 16 + }); 17 + 18 + console.log(`Server running at ${server.url}`); 19 + console.log(`Access from your network at http://192.168.1.41:${server.port}`);
+38 -40
src/ewe.gleam
··· 84 84 //// ] 85 85 //// } 86 86 //// ] 87 - //// 87 + //// 88 88 //// const callback = () => { 89 89 //// const list = document.querySelector(".sidebar > ul:last-of-type") 90 90 //// const sortedLists = document.createDocumentFragment() 91 91 //// const sortedMembers = document.createDocumentFragment() 92 - //// 92 + //// 93 93 //// for (const section of docs) { 94 94 //// sortedLists.append((() => { 95 95 //// const node = document.createElement("h3") ··· 101 101 //// node.append(section.header) 102 102 //// return node 103 103 //// })()) 104 - //// 104 + //// 105 105 //// const sortedList = document.createElement("ul") 106 106 //// sortedLists.append(sortedList) 107 - //// 107 + //// 108 108 //// const sortedFunctions = [...section.functions].sort() 109 - //// 109 + //// 110 110 //// for (const funcName of sortedFunctions) { 111 111 //// const href = `#${funcName}` 112 112 //// const member = document.querySelector( ··· 117 117 //// sortedMembers.append(member) 118 118 //// } 119 119 //// } 120 - //// 120 + //// 121 121 //// document.querySelector(".sidebar").insertBefore(sortedLists, list) 122 122 //// document 123 123 //// .querySelector(".module-members:has(#module-values)") ··· 126 126 //// document.querySelector("#module-values").nextSibling 127 127 //// ) 128 128 //// } 129 - //// 129 + //// 130 130 //// document.readyState !== "loading" 131 131 //// ? callback() 132 132 //// : document.addEventListener( ··· 135 135 //// { once: true } 136 136 //// ) 137 137 //// </script> 138 - 139 - // ----------------------------------------------------------------------------- 140 - // IMPORTS 141 - // ----------------------------------------------------------------------------- 142 138 143 139 import ewe/internal/file 144 140 import ewe/internal/handler ··· 192 188 /// 193 189 pub fn ip_address_to_string(address address: IpAddress) -> String { 194 190 ewe_to_glisten_ip(address) 195 - |> glisten.ip_address_to_string() 191 + |> glisten.ip_address_to_string 196 192 } 197 193 198 194 fn glisten_to_ewe_ip(ip: glisten.IpAddress) -> IpAddress { ··· 618 614 /// 619 615 pub type BodyError { 620 616 /// Body is larger than the provided limit. 617 + /// 621 618 BodyTooLarge 622 619 /// Body is malformed. 620 + /// 623 621 InvalidBody 624 622 } 625 623 ··· 691 689 // CHUNKED RESPONSE 692 690 // ----------------------------------------------------------------------------- 693 691 694 - /// Represents a chunked response body. This type is used to send a chunked 692 + /// Represents a chunked response body. This type is used to send a chunked 695 693 /// response to the client. 696 - /// 694 + /// 697 695 pub type ChunkedBody = 698 696 chunked.ChunkedBody 699 697 ··· 710 708 } 711 709 712 710 /// Instructs chunked response to continue processing. 713 - /// 711 + /// 714 712 pub fn chunked_continue(user_state: user_state) -> ChunkedNext(user_state) { 715 713 ChunkedContinue(user_state) 716 714 } ··· 738 736 } 739 737 740 738 /// Sets up the connection for chunked response. 741 - /// 742 - /// `on_init` function is called once the chunked response process is 743 - /// initialized. The argument is subject that can be used to send chunks to the 739 + /// 740 + /// `on_init` function is called once the chunked response process is 741 + /// initialized. The argument is subject that can be used to send chunks to the 744 742 /// client. It must return initial state. 745 743 /// 746 744 /// `handler` function is called for every message received. It must return ··· 992 990 ) 993 991 } 994 992 995 - /// WebSocket close codes that can be sent when closing a connection. The `data` 996 - /// parameter allows you to include payload up to 123 bytes in size. 997 - /// 993 + /// WebSocket close codes that can be sent when closing a connection. The `data` 994 + /// parameter allows you to include payload up to 123 bytes in size. 995 + /// 998 996 pub type CloseCode { 999 - /// Standard graceful shutdown (1000). Use when connection completed 997 + /// Standard graceful shutdown (1000). Use when connection completed 1000 998 /// successfully. 1001 - /// 999 + /// 1002 1000 NormalClosure(data: String) 1003 - /// Invalid message format (1007). Received payload that doesn't match what 1001 + /// Invalid message format (1007). Received payload that doesn't match what 1004 1002 /// you expected. 1005 - /// 1003 + /// 1006 1004 InvalidPayloadData(data: String) 1007 1005 /// Application policy violation (1008).Client broke your rules - failed 1008 1006 /// authentication, hit rate limits, or violated business logic. 1009 - /// 1007 + /// 1010 1008 PolicyViolation(data: String) 1011 - /// Message exceeds size limits (1009). Client sent something bigger than 1009 + /// Message exceeds size limits (1009). Client sent something bigger than 1012 1010 /// your application allows. 1013 - /// 1011 + /// 1014 1012 MessageTooBig(data: String) 1015 - /// Server encountered unexpected error (1011). Something went wrong on your 1013 + /// Server encountered unexpected error (1011). Something went wrong on your 1016 1014 /// side that prevents handling the connection. 1017 - /// 1015 + /// 1018 1016 InternalError(data: String) 1019 - /// Server is restarting (1012). Planned restart - clients can reconnect 1017 + /// Server is restarting (1012). Planned restart - clients can reconnect 1020 1018 /// after a bit. 1021 - /// 1019 + /// 1022 1020 ServiceRestart(data: String) 1023 - /// Temporary server overload (1013). Use when server is temporarily 1021 + /// Temporary server overload (1013). Use when server is temporarily 1024 1022 /// unavailable, client should retry. 1025 - /// 1023 + /// 1026 1024 TryAgainLater(data: String) 1027 - /// Gateway/proxy received invalid response (1014). You're acting as a proxy 1025 + /// Gateway/proxy received invalid response (1014). You're acting as a proxy 1028 1026 /// and the upstream server gave you garbage. 1029 - /// 1027 + /// 1030 1028 BadGateway(data: String) 1031 1029 /// Custom close codes 3000-4999 for application-specific use. 1032 - /// 1030 + /// 1033 1031 CustomCloseCode(code: Int, data: String) 1034 1032 /// Close without a specific reason. 1035 - /// 1033 + /// 1036 1034 NoCloseReason 1037 1035 } 1038 1036 ··· 1120 1118 /// - `id`: event ID. 1121 1119 /// - `retry`: The reconnection time. If the connection to the server is lost, 1122 1120 /// the browser will wait for the specified time before attempting to reconnect. 1123 - /// 1121 + /// 1124 1122 /// Can be created using `ewe.event` and modified with `ewe.event_name`, 1125 1123 /// `ewe.event_id`, and `ewe.event_retry`. 1126 1124 /// ··· 1160 1158 /// 1161 1159 /// `handler` function is called for every subject's message received. It must 1162 1160 /// return instruction on how SSE connection should proceed. 1163 - /// 1161 + /// 1164 1162 /// `on_close` function is called when SSE process is going to be stopped. 1165 1163 /// 1166 1164 pub fn sse(
-1
src/ewe/internal/http2.gleam
··· 1 -
+50 -68
src/ewe/internal/stream/websocket.gleam
··· 1 1 import exception 2 2 import gleam/bit_array 3 3 import gleam/bytes_tree 4 - import gleam/dynamic/decode 4 + import gleam/dynamic 5 5 import gleam/erlang/atom 6 6 import gleam/erlang/process.{type Selector, type Subject} 7 7 import gleam/option.{type Option, None, Some} ··· 39 39 AbnormalStop(reason: String) 40 40 } 41 41 42 - /// Internal state maintained by the WebSocket actor. 43 - /// 42 + // Internal state maintained by the WebSocket actor. 43 + // 44 44 type WebsocketState(user_state) { 45 45 WebsocketState(user_state: user_state, context: websocks.Context) 46 46 } 47 47 48 - /// Type alias for actor next steps. 49 - /// 48 + // Type alias for actor next steps. 49 + // 50 50 type ActorNext(user_state, user_message) = 51 51 actor.Next(WebsocketState(user_state), InternalMessage(user_message)) 52 52 53 - /// Internal messages used by the WebSocket actor. 54 - /// 53 + // Internal messages used by the WebSocket actor. 54 + // 55 55 type InternalMessage(user_message) { 56 56 Packet(BitArray) 57 57 Close ··· 60 60 Invalid 61 61 } 62 62 63 - /// Function called when the WebSocket connection is initialized. 64 - /// 63 + // Function called when the WebSocket connection is initialized. 64 + // 65 65 type OnInit(user_state, user_message) = 66 66 fn(WebsocketConnection, Selector(user_message)) -> 67 67 #(user_state, Selector(user_message)) 68 68 69 - /// Function called to handle incoming WebSocket messages. 70 - /// 69 + // Function called to handle incoming WebSocket messages. 70 + // 71 71 type Handler(user_state, user_message) = 72 72 fn(WebsocketConnection, user_state, WebsocketMessage(user_message)) -> 73 73 WebsocketNext(user_state, user_message) 74 74 75 - /// Function called when the WebSocket connection is closed. 76 - /// 75 + // Function called when the WebSocket connection is closed. 76 + // 77 77 type OnClose(user_state) = 78 78 fn(WebsocketConnection, user_state) -> Nil 79 79 80 - /// Error message for malformed messages. 81 - /// 80 + // Error message for malformed messages. 81 + // 82 82 const malformed = "Received malformed message" 83 83 84 - /// Error message for crashed WebSocket handler. 85 - /// 84 + // Error message for crashed WebSocket handler. 85 + // 86 86 const crashed = "Crash in websocket handler" 87 87 88 - /// Error message for failed PONG frame. 89 - /// 88 + // Error message for failed PONG frame. 89 + // 90 90 const failed_pong = "Failed to send PONG frame" 91 91 92 - /// Error message for sending WebSocket message from non-owning process. 93 - /// 92 + // Error message for sending WebSocket message from non-owning process. 93 + // 94 94 const non_owning_process = "Sending WebSocket message from non-owning process" 95 95 96 - /// Active count for socket. 97 - /// 96 + // Active count for socket. 97 + // 98 98 const socket_active_count = 100 99 99 100 100 /// Starts a new WebSocket connection. ··· 164 164 |> result.map(after_start(_, transport, socket)) 165 165 } 166 166 167 - /// Creates a selector for valid TCP/SSL records. 168 - /// 169 - fn select_valid_record( 170 - selector: Selector(InternalMessage(user_message)), 171 - binary_atom: String, 172 - ) -> Selector(InternalMessage(user_message)) { 173 - process.select_record(selector, atom.create(binary_atom), 2, fn(record) { 174 - decode.run(record, { 175 - use data <- decode.field(2, decode.bit_array) 176 - decode.success(Packet(data)) 177 - }) 178 - |> result.unwrap(Invalid) 179 - }) 180 - } 181 - 182 - /// Creates selector for glisten socket events. 183 - /// 167 + // Creates selector for glisten socket events. 168 + // 184 169 fn create_socket_selector() -> Selector(InternalMessage(user_message)) { 185 170 process.new_selector() 186 - |> select_valid_record("tcp") 187 - |> select_valid_record("ssl") 171 + |> process.select_record(atom.create("tcp"), 2, fn(record) { 172 + Packet(coerce_tcp_message(record)) 173 + }) 174 + |> process.select_record(atom.create("tcp"), 2, fn(record) { 175 + Packet(coerce_tcp_message(record)) 176 + }) 188 177 |> process.select_record(atom.create("tcp_closed"), 1, fn(_) { Close }) 189 178 |> process.select_record(atom.create("ssl_closed"), 1, fn(_) { Close }) 190 179 |> process.select_record(atom.create("tcp_passive"), 1, fn(_) { TcpPassive }) 191 180 } 192 181 193 - /// Handles incoming packet data, decoding frames and processing them. 194 - /// 182 + @external(erlang, "ewe_ffi", "coerce_tcp_message") 183 + fn coerce_tcp_message(record: dynamic.Dynamic) -> BitArray 184 + 185 + // Handles incoming packet data, decoding frames and processing them. 186 + // 195 187 fn handle_valid_packet( 196 188 transport: Transport, 197 189 socket: Socket, ··· 201 193 on_close: OnClose(user_state), 202 194 ) -> ActorNext(user_state, user_message) { 203 195 let conn = WebsocketConnection(transport, socket, state.context) 204 - let result = 196 + let processed = 205 197 websocks.process_incoming_frames( 206 198 data, 207 199 state.context, ··· 214 206 handle_frame, 215 207 ) 216 208 217 - case result { 209 + case processed { 218 210 Ok(#(resolved_state, context)) -> { 219 211 case resolved_state.next { 220 212 Continue(user_state, selector) -> { ··· 230 222 handle_close(on_close, state, conn, Some(reason)) 231 223 } 232 224 } 233 - Error(_violation) -> { 234 - // echo violation as "violation during frame resolving" 235 - handle_close(on_close, state, conn, Some(malformed)) 236 - } 225 + Error(_violation) -> handle_close(on_close, state, conn, Some(malformed)) 237 226 } 238 227 } 239 228 240 - /// Represents the state of the WebSocket connection when resolving frames. 241 - /// 229 + // Represents the state of the WebSocket connection when resolving frames. 230 + // 242 231 type ResolveState(user_state, user_message) { 243 232 ResolveState( 244 233 socket: Socket, ··· 286 275 } 287 276 } 288 277 } 278 + 289 279 websocks.Control(websocks.Close(reason)) -> { 290 280 let _ = 291 281 transport.send( ··· 309 299 case call { 310 300 Ok(Continue(user_state, new_selector)) -> { 311 301 let next_selector = 312 - user_selector(new_selector) 302 + option.map(new_selector, process.map_selector(_, User)) 313 303 |> option.or(selector) 314 304 |> option.map(process.merge_selector(create_socket_selector(), _)) 315 305 ··· 327 317 } 328 318 } 329 319 330 - /// Handles user messages sent to the WebSocket. 331 - /// 320 + // Handles user messages sent to the WebSocket. 321 + // 332 322 fn handle_user_message( 333 323 transport: Transport, 334 324 socket: Socket, ··· 346 336 case call { 347 337 Ok(Continue(new_user_state, new_selector)) -> { 348 338 let next_selector = 349 - user_selector(new_selector) 339 + option.map(new_selector, process.map_selector(_, User)) 350 340 |> option.map(process.merge_selector(create_socket_selector(), _)) 351 341 352 342 let next = ··· 364 354 } 365 355 } 366 356 367 - /// Maps user selector to internal message. 368 - /// 369 - fn user_selector( 370 - selector: Option(Selector(user_message)), 371 - ) -> Option(Selector(InternalMessage(user_message))) { 372 - option.map(selector, fn(selector) { process.map_selector(selector, User) }) 373 - } 374 - 375 - /// Handles WebSocket connection closure. 376 - /// 357 + // Handles WebSocket connection closure. 358 + // 377 359 fn handle_close( 378 360 on_close: OnClose(user_state), 379 361 state: WebsocketState(user_state), ··· 395 377 } 396 378 } 397 379 398 - /// Maps actor's starting value to Nil. 399 - /// 380 + // Maps actor's starting value to Nil. 381 + // 400 382 fn after_start( 401 383 started: actor.Started(Subject(InternalMessage(user_message))), 402 384 transport: Transport,
+7 -16
src/ewe_ffi.erl
··· 1 1 -module(ewe_ffi). 2 2 3 3 -export([close_file/1, decode_packet/3, init_clock_storage/0, lookup_http_date/0, now/0, 4 - now_microseconds/0, open_file/1, rescue/1, set_http_date/1, validate_field_value/1]). 4 + now_microseconds/0, open_file/1, set_http_date/1, validate_field_value/1, coerce_tcp_message/1]). 5 + 6 + % Socket 7 + % ----------------------------------------------------------------------------- 8 + 9 + coerce_tcp_message({tcp, _Socket, Data}) -> Data; 10 + coerce_tcp_message({ssl, _Socket, Data}) -> Data. 5 11 6 12 % HTTP 7 13 % ----------------------------------------------------------------------------- ··· 110 116 {error, _} -> 111 117 {error, eunknown} 112 118 end. 113 - 114 - % RESCUING 115 - % ----------------------------------------------------------------------------- 116 - 117 - rescue(Callable) -> 118 - try 119 - {ok, Callable()} 120 - catch 121 - error:Error -> 122 - {error, {errored, Error}}; 123 - Error -> 124 - {error, {thrown, Error}}; 125 - exit:Error -> 126 - {error, {exited, Error}} 127 - end.
+1 -1
test/autobahn.gleam
··· 30 30 }) 31 31 |> ewe.enable_ipv6() 32 32 |> ewe.bind_all() 33 - |> ewe.listening(port: 8080) 33 + |> ewe.listening(port: 8081) 34 34 |> ewe.supervised() 35 35 36 36 let assert Ok(_) =
+50
test/bench.gleam
··· 1 + import ewe 2 + import gleam/erlang/process 3 + import gleam/http 4 + import gleam/http/request.{type Request} 5 + import gleam/http/response 6 + import gleam/result 7 + 8 + pub fn main() { 9 + let empty_response = 10 + response.new(200) 11 + |> response.set_body(ewe.Empty) 12 + 13 + let not_found = 14 + response.new(404) 15 + |> response.set_body(ewe.Empty) 16 + 17 + let too_large = 18 + response.new(413) 19 + |> response.set_body(ewe.Empty) 20 + 21 + let assert Ok(_) = 22 + fn(req: Request(ewe.Connection)) { 23 + case req.method, request.path_segments(req) { 24 + http.Get, [] -> empty_response 25 + http.Get, ["user", id] -> 26 + response.new(200) |> response.set_body(ewe.TextData(id)) 27 + http.Post, ["user"] -> { 28 + case ewe.read_body(req, 40_000_000) { 29 + Ok(req) -> { 30 + let content_type = 31 + req 32 + |> request.get_header("content-type") 33 + |> result.unwrap("application/octet-stream") 34 + 35 + response.new(200) 36 + |> response.set_body(ewe.BitsData(req.body)) 37 + |> response.prepend_header("content-type", content_type) 38 + } 39 + Error(_) -> too_large 40 + } 41 + } 42 + _, _ -> not_found 43 + } 44 + } 45 + |> ewe.new 46 + |> ewe.listening(port: 8080) 47 + |> ewe.start() 48 + 49 + process.sleep_forever() 50 + }