๐Ÿ‘ a fluffy Gleam web server
23

Configure Feed

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

improve http/2 related connections

vshakitskiy (Feb 23, 2026, 1:53 PM +0300) 7be7cdb8 b7331354

+102 -44
+2
.gitignore
··· 2 2 *.ez 3 3 /build 4 4 erl_crash.dump 5 + .claude 6 + CLAUDE.md 5 7 /src/preview.gleam 6 8 autobahn/server 7 9 # examples
+5
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + # Unreleased 4 + 5 + - Gracefully handle HTTP/2 connections: h2c upgrade requests are served as HTTP/1.1, and direct HTTP/2 connections receive a GOAWAY with HTTP_1_1_REQUIRED instead of being silently dropped. 6 + - Fixed HTTP/2 prior-knowledge detection in ffi 7 + 3 8 # v3.0.1 - 25.01.2026 4 9 5 10 - Fix README file
+2 -2
examples/manifest.toml
··· 3 3 4 4 packages = [ 5 5 { name = "compresso", version = "0.1.0", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_stdlib", "gleam_yielder", "logging"], otp_app = "compresso", source = "hex", outer_checksum = "8BE29A1EDA42F70826ED148EAE40C46BB3FC18E78FE472663DB01DD4A38172D4" }, 6 - { name = "ewe", version = "2.1.2", build_tools = ["gleam"], requirements = ["compresso", "exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "logging", "websocks"], source = "local", path = ".." }, 6 + { name = "ewe", version = "3.0.1", build_tools = ["gleam"], requirements = ["compresso", "exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "logging", "websocks"], source = "local", path = ".." }, 7 7 { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 8 8 { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 9 9 { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 10 10 { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 11 11 { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 12 - { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" }, 12 + { name = "gleam_stdlib", version = "0.69.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AAB0962BEBFAA67A2FBEE9EEE218B057756808DC9AF77430F5182C6115B3A315" }, 13 13 { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 14 14 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 15 15 { name = "glisten", version = "8.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "86B838196592D9EBDE7A1D2369AE3A51E568F7DD2D168706C463C42D17B95312" },
+1 -1
examples/src/https.gleam
··· 5 5 6 6 pub fn main() { 7 7 logging.configure() 8 - logging.set_level(logging.Info) 8 + logging.set_level(logging.Debug) 9 9 10 10 // Start the server that has TLS enabled. 11 11 let assert Ok(_) =
+2
src/ewe.gleam
··· 503 503 |> fn(glisten_builder) { 504 504 case builder.tls { 505 505 Some(#(cert, key)) -> glisten.with_tls(glisten_builder, cert, key) 506 + // Uncomment once http2 will be implemented! 507 + // |> glisten.with_http2 506 508 None -> glisten_builder 507 509 } 508 510 }
+21 -15
src/ewe/internal/handler.gleam
··· 1 1 import ewe/internal/http1.{type Connection, type ResponseBody} as ewe_http 2 2 import ewe/internal/http1/handler as http1_handler 3 + import gleam/bytes_tree 3 4 import gleam/erlang/process 4 5 import gleam/http/request.{type Request} 5 6 import gleam/http/response.{type Response} ··· 7 8 import gleam/otp/actor 8 9 import gleam/otp/factory_supervisor as factory 9 10 import glisten 11 + import glisten/transport 10 12 import logging 11 13 12 14 /// State of the request handler. 13 - /// 15 + /// 14 16 pub type Handler { 15 17 Http1(state: http1_handler.Http1Handler, self: process.Subject(Nil)) 16 18 } 17 19 18 20 /// Initializes the request handler state. 19 - /// 21 + /// 20 22 pub fn init(_) -> #(Handler, Option(process.Selector(Nil))) { 21 23 let subject = process.new_subject() 22 24 let selector = ··· 27 29 } 28 30 29 31 /// Main loop that processes incoming messages. 30 - /// 32 + /// 31 33 pub fn loop( 32 34 handler: fn(Request(Connection)) -> Response(ResponseBody), 33 35 on_crash: Response(ResponseBody), ··· 59 61 60 62 case result { 61 63 http1_handler.Continue(state) -> glisten.continue(Http1(state, self)) 62 - http1_handler.Http2Upgrade(http1_handler.OverCleartext( 63 - _req, 64 - _settings, 65 - )) -> { 66 - logging.log( 67 - logging.Debug, 68 - "Received HTTP/2 cleartext upgrade request", 69 - ) 64 + http1_handler.Http2Upgrade(http1_handler.Direct(_data)) -> { 65 + logging.log(logging.Notice, "HTTP/2 upgrade; sending goaway") 66 + let goaway = << 67 + 0:size(24), 4, 0, 0:size(32), 8:size(24), 7, 0, 0:size(32), 68 + 0:size(32), 13:size(32), 69 + >> 70 + 71 + let _ = 72 + transport.send( 73 + conn.transport, 74 + conn.socket, 75 + bytes_tree.from_bit_array(goaway), 76 + ) 77 + 70 78 glisten.stop() 71 79 } 72 - http1_handler.Http2Upgrade(_data) -> { 73 - logging.log(logging.Debug, "Received HTTP/2 upgrade request") 80 + http1_handler.Stop 81 + | http1_handler.Http2Upgrade(http1_handler.Upgrade(..)) -> 74 82 glisten.stop() 75 - } 76 - http1_handler.Stop -> glisten.stop() 77 83 } 78 84 } 79 85 _, _ -> glisten.stop()
+5 -5
src/ewe/internal/http1.gleam
··· 120 120 } 121 121 122 122 /// HTTP/2 upgrade options. 123 - /// 123 + /// 124 124 pub type Http2Upgrade { 125 - OverCleartext(req: Request(Connection), settings: String) 126 - OverTLS(data: BitArray) 125 + Upgrade(req: Request(Connection), settings: String) 126 + Direct(data: BitArray) 127 127 } 128 128 129 129 /// Parses an HTTP request from the given buffer. ··· 207 207 string.contains(string.lowercase(connection), "upgrade") 208 208 209 209 case is_upgrade { 210 - True -> Ok(Http2Upgrade(OverCleartext(req:, settings:))) 210 + True -> Ok(Http2Upgrade(Upgrade(req:, settings:))) 211 211 False -> Ok(Http1Request(req:, version: Http11)) 212 212 } 213 213 } ··· 218 218 } 219 219 } 220 220 Ok(Packet(decoder.Http2Upgrade, <<"\r\nSM\r\n\r\n":utf8, data:bits>>)) -> 221 - Ok(Http2Upgrade(OverTLS(data:))) 221 + Ok(Http2Upgrade(Direct(data:))) 222 222 Ok(More(size)) -> { 223 223 use new_buffer <- try(read_from_socket( 224 224 transport,
+33 -18
src/ewe/internal/http1/handler.gleam
··· 24 24 import logging 25 25 26 26 /// HTTP/1.1 handler state. 27 - /// 27 + /// 28 28 pub type Http1Handler { 29 29 Http1Handler(idle_timer: Option(process.Timer)) 30 30 } 31 31 32 32 /// Initializes the HTTP/1.1 handler state. 33 - /// 33 + /// 34 34 pub fn init() -> Http1Handler { 35 35 Http1Handler(idle_timer: None) 36 36 } 37 37 38 38 /// Action to take after handling a packet. 39 - /// 39 + /// 40 40 pub type Next { 41 41 Continue(state: Http1Handler) 42 42 Stop ··· 44 44 } 45 45 46 46 /// HTTP/2 upgrade options. 47 - /// 47 + /// 48 48 pub type Http2Upgrade { 49 - OverCleartext(request: Request(Connection), settings: String) 50 - OverTLS(data: BitArray) 49 + Upgrade(request: Request(Connection), settings: String) 50 + Direct(data: BitArray) 51 51 } 52 52 53 53 /// Handles received glisten packet. 54 - /// 54 + /// 55 55 pub fn handle_packet( 56 56 state: Http1Handler, 57 57 connection: Connection, ··· 76 76 Error(Nil) -> Stop 77 77 } 78 78 } 79 - Ok(ewe_http.Http2Upgrade(ewe_http.OverTLS(data))) -> 80 - Http2Upgrade(OverTLS(data:)) 81 - Ok(ewe_http.Http2Upgrade(ewe_http.OverCleartext(request, settings))) -> 82 - Http2Upgrade(OverCleartext(request:, settings:)) 79 + Ok(ewe_http.Http2Upgrade(ewe_http.Direct(data))) -> 80 + Http2Upgrade(Direct(data:)) 81 + Ok(ewe_http.Http2Upgrade(ewe_http.Upgrade(request, _settings))) -> { 82 + logging.log(logging.Notice, "HTTP/2 upgrade; using HTTP/1.1") 83 + let call_result = 84 + call( 85 + request, 86 + ewe_http.Http11, 87 + glisten_subject, 88 + handler, 89 + on_crash, 90 + idle_timeout, 91 + ) 92 + 93 + case call_result { 94 + Ok(state) -> Continue(state) 95 + Error(Nil) -> Stop 96 + } 97 + } 83 98 Error(reason) -> { 84 99 let status = case reason { 85 100 ewe_http.InvalidVersion -> 505 ··· 99 114 } 100 115 101 116 /// Takes parsed HTTP request and calls the handler. 102 - /// 117 + /// 103 118 fn call( 104 119 request: Request(Connection), 105 120 version: HttpVersion, ··· 130 145 } 131 146 132 147 /// Actions to take after response is sent. 133 - /// 148 + /// 134 149 fn on_sent( 135 150 sent: Result(Nil, glisten.SocketReason), 136 151 response: Response(ResponseBody), ··· 149 164 } 150 165 151 166 /// Sends a file to the client. 152 - /// 167 + /// 153 168 fn send_file( 154 169 request: Request(Connection), 155 170 version: HttpVersion, ··· 185 200 } 186 201 187 202 /// Sends a body to the client. 188 - /// 203 + /// 189 204 fn send_body( 190 205 request: Request(Connection), 191 206 version: HttpVersion, ··· 236 251 } 237 252 238 253 /// Can the body be encoded to gzip? 239 - /// 254 + /// 240 255 fn can_encode_gzip(request: Request(Connection), response: Response(_)) -> Bool { 241 256 let accept_encoding = 242 257 request.get_header(request, "accept-encoding") ··· 251 266 } 252 267 253 268 /// Removes the charset from the content-type header. 254 - /// 269 + /// 255 270 fn remove_charset(response: Response(_)) -> Response(_) { 256 271 response.get_header(response, "content-type") 257 272 |> result.try(string.split_once(_, ";")) ··· 262 277 } 263 278 264 279 /// Is the connection set to close? 265 - /// 280 + /// 266 281 fn is_connection_close(response: Response(_)) -> Bool { 267 282 case response.get_header(response, "connection") { 268 283 Ok("close") -> True
+2 -2
src/ewe_ffi.erl
··· 14 14 15 15 decode_packet(Type, Packet, Options) -> 16 16 case erlang:decode_packet(Type, Packet, Options) of 17 - {ok, {http_request, <<"PRI">>, "*", {2, 0}}, Rest} -> 18 - {ok, {packet, {http2_upgrade, Rest}}}; 17 + {ok, {http_request, <<"PRI">>, '*', {2, 0}}, Rest} -> 18 + {ok, {packet, http2_upgrade, Rest}}; 19 19 {ok, {http_request, Method, Uri, Version}, Rest} -> 20 20 {ok, {packet, {http_request, atom_to_binary(Method), Uri, Version}, Rest}}; 21 21 {ok, {http_header, Idx, _, Field, Value}, Rest} ->
+28
test/https.gleam
··· 1 + import ewe.{type Request, type Response} 2 + import gleam/erlang/process 3 + import gleam/http/response 4 + import logging 5 + 6 + pub fn main() { 7 + logging.configure() 8 + logging.set_level(logging.Debug) 9 + 10 + // Start the server that has TLS enabled. 11 + let assert Ok(_) = 12 + ewe.new(handler) 13 + |> ewe.bind("0.0.0.0") 14 + |> ewe.listening(port: 8080) 15 + |> ewe.enable_tls( 16 + certificate_file: "examples/priv/localhost.crt", 17 + key_file: "examples/priv/localhost.key", 18 + ) 19 + |> ewe.start 20 + 21 + process.sleep_forever() 22 + } 23 + 24 + fn handler(_req: Request) -> Response { 25 + response.new(200) 26 + |> response.set_header("content-type", "text/plain; charset=utf-8") 27 + |> response.set_body(ewe.TextData("Hello, World!")) 28 + }
+1 -1
test/preview.gleam
··· 19 19 20 20 pub fn main() { 21 21 logging.configure() 22 - logging.set_level(logging.Info) 22 + logging.set_level(logging.Debug) 23 23 24 24 // Create a named subject for the pubsub worker 25 25 let pubsub_name = process.new_name("pubsub")