๐Ÿ‘ a fluffy Gleam web server
23

Configure Feed

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

remove unsafe type manipulations

vshakitskiy (Jul 24, 2026, 1:10 PM +0300) bee55998 22efcd2c

+128 -133
+61 -54
src/ewe.gleam
··· 40 40 } 41 41 42 42 pub fn ip_address_to_string(address: IpAddress) -> String { 43 - unsafe_to_internal_ip_address(address) 43 + to_internal_ip_address(address) 44 44 |> glisten.ip_address_to_string 45 45 } 46 46 47 - // IpAddress and glisten.IpAddress are structurally identical. 48 - @external(erlang, "gleam_stdlib", "identity") 49 - fn unsafe_to_internal_ip_address(address: IpAddress) -> glisten.IpAddress 47 + fn to_internal_ip_address(address: IpAddress) -> glisten.IpAddress { 48 + case address { 49 + IpV4(a, b, c, d) -> glisten.IpV4(a, b, c, d) 50 + IpV6(a, b, c, d, e, f, g, h) -> glisten.IpV6(a, b, c, d, e, f, g, h) 51 + } 52 + } 50 53 51 - // IpAddress and options.IpAddress are structurally identical. 52 - @external(erlang, "gleam_stdlib", "identity") 53 - fn unsafe_from_internal_options_ip_address( 54 - address: options.IpAddress, 55 - ) -> IpAddress 54 + fn from_internal_options_ip_address(address: options.IpAddress) -> IpAddress { 55 + case address { 56 + options.IpV4(a, b, c, d) -> IpV4(a, b, c, d) 57 + options.IpV6(a, b, c, d, e, f, g, h) -> IpV6(a, b, c, d, e, f, g, h) 58 + } 59 + } 56 60 57 - // glisten.IpAddress and IpAddress are structurally identical. 58 - @external(erlang, "gleam_stdlib", "identity") 59 - fn unsafe_from_internal_ip_address(address: glisten.IpAddress) -> IpAddress 61 + fn from_internal_ip_address(address: glisten.IpAddress) -> IpAddress { 62 + case address { 63 + glisten.IpV4(a, b, c, d) -> IpV4(a, b, c, d) 64 + glisten.IpV6(a, b, c, d, e, f, g, h) -> IpV6(a, b, c, d, e, f, g, h) 65 + } 66 + } 60 67 61 68 /// The address a socket is bound to, or the address of a connected peer. 62 69 pub type SocketAddress { ··· 68 75 fn convert_socket_address(address: glisten.SocketAddress) -> SocketAddress { 69 76 case address { 70 77 glisten.TcpSocketAddress(port:, ip_address:) -> 71 - TcpSocketAddress( 72 - ip_address: unsafe_from_internal_ip_address(ip_address), 73 - port:, 74 - ) 78 + TcpSocketAddress(ip_address: from_internal_ip_address(ip_address), port:) 75 79 glisten.UnixSocketAddress(path:) -> UnixSocketAddress(path:) 76 80 } 77 81 } ··· 80 84 /// the socket information is unavailable. 81 85 pub fn get_client_info(connection: Connection) -> Result(SocketAddress, Nil) { 82 86 case connection { 83 - connection.Http1(transport:, socket:, ..) -> { 84 - let peername = transport.peername(transport, socket) 87 + connection.Http1(connection) -> { 88 + let peername = transport.peername(connection.transport, connection.socket) 85 89 use info <- result.map(over: peername) 86 90 87 91 case info { 88 92 socket.TcpSockName(ip_address:, port:) -> 89 - unsafe_from_internal_options_ip_address(ip_address) 93 + from_internal_options_ip_address(ip_address) 90 94 |> TcpSocketAddress(port:) 91 95 socket.UnixSockName(path:) -> UnixSocketAddress(path:) 92 96 } ··· 128 132 PrivateKeyInfo 129 133 } 130 134 131 - // TlsKeyType and options.TlsKeyType are structurally identical. 132 - @external(erlang, "gleam_stdlib", "identity") 133 - fn unsafe_to_internal_tls_key_type(key_type: TlsKeyType) -> options.TlsKeyType 135 + fn to_internal_tls_key_type(key_type: TlsKeyType) -> options.TlsKeyType { 136 + case key_type { 137 + RsaPrivateKey -> options.RsaPrivateKey 138 + EcPrivateKey -> options.EcPrivateKey 139 + DsaPrivateKey -> options.DsaPrivateKey 140 + PrivateKeyInfo -> options.PrivateKeyInfo 141 + } 142 + } 134 143 135 144 /// Contains all server configurations, can be adjusted by different builder 136 145 /// functions. ··· 283 292 } 284 293 285 294 // Body and connection.Body are structurally identical. 286 - @external(erlang, "gleam_stdlib", "identity") 295 + @external(erlang, "ewe_ffi", "identity") 287 296 fn unsafe_to_internal_response( 288 297 response: response.Response(Body), 289 298 ) -> response.Response(connection.Body) ··· 313 322 glisten.with_tls_der( 314 323 pool, 315 324 cert:, 316 - key_type: unsafe_to_internal_tls_key_type(key_type), 325 + key_type: to_internal_tls_key_type(key_type), 317 326 key:, 318 327 ) 319 328 None -> pool ··· 363 372 InvalidLimit 364 373 } 365 374 366 - // FileError and file.FileError are structurally identical. 367 - @external(erlang, "gleam_stdlib", "identity") 368 - fn unsafe_from_internal_file_error(error: file.FileError) -> FileError 375 + fn from_internal_file_error(error: file.FileError) -> FileError { 376 + case error { 377 + file.NotFound -> NotFound 378 + file.IsDirectory -> IsDirectory 379 + file.AccessDenied -> AccessDenied 380 + file.UnknownError -> UnknownError 381 + file.InvalidOffset -> InvalidOffset 382 + file.InvalidLimit -> InvalidLimit 383 + } 384 + } 369 385 370 386 /// Prepares a file to be streamed as a response body. `offset` and `limit` in 371 387 /// bytes let you serve a byte range from the file. leave either as `None` to ··· 377 393 ) -> Result(Body, FileError) { 378 394 case file.resolve(path, offset, limit) { 379 395 Ok(file) -> Ok(File(file)) 380 - Error(error) -> Error(unsafe_from_internal_file_error(error)) 396 + Error(error) -> Error(from_internal_file_error(error)) 381 397 } 382 398 } 383 399 ··· 389 405 InvalidBody 390 406 } 391 407 392 - // BodyError and http1.BodyError are structurally identical. 393 - @external(erlang, "gleam_stdlib", "identity") 394 - fn unsafe_from_internal_body_error(error: http1.BodyError) -> BodyError 408 + fn from_internal_http1_body_error(error: http1.BodyError) -> BodyError { 409 + case error { 410 + http1.BodyTooLarge -> BodyTooLarge 411 + http1.InvalidBody -> InvalidBody 412 + } 413 + } 395 414 396 415 /// Reads the entire request body into memory, up to `limit` bytes. For a 397 416 /// chunked request, any trailer fields are appended to the returned request's ··· 401 420 limit limit: Int, 402 421 ) -> Result(request.Request(BitArray), BodyError) { 403 422 case req.body { 404 - connection.Http1(..) -> { 423 + connection.Http1(connection) -> { 405 424 use #(body, trailers) <- result.try( 406 - http1.read_body(http1.unsafe_to_http1_connection(req.body), limit) 407 - |> result.map_error(unsafe_from_internal_body_error), 425 + http1.read_body(connection, limit) 426 + |> result.map_error(from_internal_http1_body_error), 408 427 ) 409 428 410 429 request.Request(..req, headers: list.append(req.headers, trailers), body:) ··· 414 433 } 415 434 } 416 435 417 - // http1.Connection and connection.Connection are structurally identical. 418 - @external(erlang, "gleam_stdlib", "identity") 419 - fn unsafe_from_http1_connection(conn: http1.Connection) -> Connection 420 - 421 436 /// The result of one `read_body_chunk` call. 422 437 pub type ReadEvent { 423 438 /// Up to `max_chunk_bytes` of body data. Feed `request` into the next call. ··· 436 451 limit limit: Int, 437 452 ) -> Result(ReadEvent, BodyError) { 438 453 case req.body { 439 - connection.Http1(..) -> { 440 - let conn = http1.unsafe_to_http1_connection(req.body) 441 - case http1.read_body_chunk(conn, max_chunk_bytes:, limit:) { 454 + connection.Http1(connection) -> { 455 + case http1.read_body_chunk(connection, max_chunk_bytes:, limit:) { 442 456 Ok(http1.Chunk(data, connection)) -> { 443 - let body = unsafe_from_http1_connection(connection) 457 + let body = connection.Http1(connection) 444 458 Ok(Chunk(data, request.set_body(req, body))) 445 459 } 446 460 Ok(http1.Done(trailers)) -> { 447 461 let headers = list.append(req.headers, trailers) 448 462 Ok(Done(request.Request(..req, headers:, body: Nil))) 449 463 } 450 - Error(error) -> Error(unsafe_from_internal_body_error(error)) 464 + Error(error) -> Error(from_internal_http1_body_error(error)) 451 465 } 452 466 } 453 467 connection.Http2 -> todo as "HTTP/2 is not implemented yet!" ··· 459 473 pub type ResponseWriter = 460 474 connection.ResponseWriter 461 475 462 - // http1.ResponseWriter and ResponseWriter are structurally identical. 463 - @external(erlang, "gleam_stdlib", "identity") 464 - fn unsafe_from_http1_writer(writer: http1.ResponseWriter) -> ResponseWriter 465 - 466 476 /// Starts a streamed response. `handler` must end by calling `finish_chunk` or 467 477 /// `finish_response` on it, since that's what closes the stream. 468 478 pub fn stream_response( ··· 477 487 /// stream in the same round trip. 478 488 pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { 479 489 case writer { 480 - connection.Http1Writer(..) -> 481 - http1.send_chunk(http1.unsafe_to_http1_writer(writer), chunk) 482 - |> unsafe_from_http1_writer 490 + connection.Http1Writer(writer) -> 491 + connection.Http1Writer(http1.send_chunk(writer, chunk)) 483 492 connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" 484 493 } 485 494 } ··· 487 496 /// Sends `chunk` as the final response body chunk and closes the stream. 488 497 pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { 489 498 case writer { 490 - connection.Http1Writer(..) -> 491 - http1.finish_chunk(http1.unsafe_to_http1_writer(writer), chunk) 499 + connection.Http1Writer(writer) -> http1.finish_chunk(writer, chunk) 492 500 connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" 493 501 } 494 502 } ··· 497 505 /// there's one last chunk to send. 498 506 pub fn finish_response(writer: ResponseWriter) -> Nil { 499 507 case writer { 500 - connection.Http1Writer(..) -> 501 - http1.finish_response(http1.unsafe_to_http1_writer(writer)) 508 + connection.Http1Writer(writer) -> http1.finish_response(writer) 502 509 connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" 503 510 } 504 511 }
+26 -19
src/ewe/internal/connection.gleam
··· 4 4 import glisten/transport 5 5 6 6 pub type Connection { 7 - Http1( 7 + Http1(Http1Connection) 8 + Http2 9 + } 10 + 11 + pub type Http1Connection { 12 + Http1Connection( 8 13 transport: transport.Transport, 9 14 socket: socket.Socket, 10 15 self: process.Subject(Http1Signal), ··· 12 17 framing: Framing, 13 18 // Body bytes delivered to the caller so far, via `read_body_chunk`. 14 19 read: Int, 15 - // Bytes left in the chunked-encoding chunk currently being delivered; 20 + // Bytes left in the chunked-encoding chunk currently being delivered. 16 21 // 0 means the next pull starts at a chunk boundary. Unused for `Fixed` 17 22 // and `NoBody`. 18 23 chunk_remaining: Int, 19 24 ) 20 - Http2 21 25 } 22 26 23 27 /// How to find the end of a request body on the wire, derived once from ··· 45 49 Timeout 46 50 } 47 51 48 - /// Sent by `http1.gleam` to a request's own private subject, drained 49 - /// synchronously within the same `http1.handle_message` call 52 + /// Sent to a http1 request's own subject, drained synchronously within the same 53 + /// `http1.handle_message` call 50 54 pub type Http1Signal { 51 - /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves 52 - /// once the request body has been fully consumed, carrying whatever bytes 53 - /// came after it. 55 + /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves once 56 + /// the request body has been fully consumed, carrying whatever bytes came 57 + /// after it. 54 58 BodyDrained(leftover: BitArray) 55 - /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves 56 - /// when they gave up on the body part-way through, leaving the connection 57 - /// in an unknown position. 59 + /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves when 60 + /// they gave up on the body part way through, leaving the connection in an 61 + /// unknown position. 58 62 BodyAbandoned 59 - /// Sent by `http1.read_body_chunk` to itself after every chunk it 60 - /// delivers, so that if the caller stops reading before `BodyDrained`, 61 - /// the connection can still resume draining from here instead of from the 62 - /// start of the body. 63 + /// Sent by `http1.read_body_chunk` to itself after every chunk it delivers, 64 + /// so that if the caller stops reading before `BodyDrained`, the connection 65 + /// can still resume draining from here instead of from the start of the body. 63 66 BodyProgress(buffer: BitArray, read: Int, chunk_remaining: Int) 64 - /// Sent by `http1.finish_chunk` and `http1.finish_response` to themselves once 65 - /// a streamed response's terminator has been written. 67 + /// Sent by `http1.finish_chunk` and `http1.finish_response` to themselves 68 + /// once a streamed response's terminator has been written. 66 69 StreamFinished(keep_alive: Bool) 67 70 } 68 71 69 72 /// How a streamed response writes its body chunks to the wire. 70 73 pub type ResponseWriter { 71 - Http1Writer( 74 + Http1Writer(Http1ResponseWriter) 75 + Http2Writer 76 + } 77 + 78 + pub type Http1ResponseWriter { 79 + Http1ResponseWriter( 72 80 transport: transport.Transport, 73 81 socket: socket.Socket, 74 82 self: process.Subject(Http1Signal), ··· 82 90 // `False` when `chunked` is `False`. 83 91 keep_alive: Bool, 84 92 ) 85 - Http2Writer 86 93 }
+4 -1
src/ewe/internal/ewe_ffi.erl
··· 1 1 -module(ewe_ffi). 2 2 3 - -export([now_datetime/0, set_http_date/1, get_http_date/0]). 3 + -export([identity/1, now_datetime/0, set_http_date/1, get_http_date/0]). 4 + 5 + identity(X) -> 6 + X. 4 7 5 8 now_datetime() -> 6 9 {Date, Time} = calendar:universal_time(),
-2
src/ewe/internal/file_ffi.erl
··· 13 13 {error, _Reason} -> {error, unknown_error} 14 14 end. 15 15 16 - %% the kernel streams the file straight to the socket without passing through 17 - %% userspace memory. 18 16 sendfile(Fd, Socket, Offset, Bytes) -> 19 17 case file:sendfile(Fd, Socket, Offset, Bytes, []) of 20 18 {ok, _Sent} -> {ok, nil};
+37 -57
src/ewe/internal/http1.gleam
··· 53 53 let self = process.new_subject() 54 54 55 55 let body_connection = 56 - connection.Http1( 56 + connection.Http1Connection( 57 57 transport: connection.transport, 58 58 socket: connection.socket, 59 59 self:, ··· 67 67 request.Request( 68 68 method: head.method, 69 69 headers: head.headers, 70 - body: body_connection, 70 + body: connection.Http1(body_connection), 71 71 scheme:, 72 72 host: head.host, 73 73 port: head.port, ··· 79 79 80 80 let drained = drain_messages(self) 81 81 82 - let #(buffer, body_drained) = 83 - resolve_body(unsafe_to_http1_connection(body_connection), drained.body) 82 + let #(buffer, body_drained) = resolve_body(body_connection, drained.body) 84 83 85 84 let metadata = 86 85 Metadata(..metadata, keep_alive: metadata.keep_alive && body_drained) ··· 104 103 } 105 104 option.None, option.Some(Stream(handler: stream_handler, chunked:)) 106 105 -> { 107 - connection.Http1Writer( 106 + connection.Http1Writer(connection.Http1ResponseWriter( 108 107 transport: connection.transport, 109 108 socket: connection.socket, 110 109 self:, 111 110 chunked:, 112 111 keep_alive:, 113 - ) 112 + )) 114 113 |> stream_handler 115 114 116 115 let stream_drained = drain_messages(self) ··· 213 212 InvalidBody 214 213 } 215 214 216 - pub type Connection { 217 - Http1( 218 - transport: transport.Transport, 219 - socket: socket.Socket, 220 - self: process.Subject(connection.Http1Signal), 221 - buffer: BitArray, 222 - framing: connection.Framing, 223 - read: Int, 224 - chunk_remaining: Int, 225 - ) 226 - } 227 - 228 - @external(erlang, "gleam_stdlib", "identity") 229 - pub fn unsafe_to_http1_connection(conn: connection.Connection) -> Connection 215 + pub type Connection = 216 + connection.Http1Connection 230 217 231 218 const body_read_timeout = 10_000 232 219 ··· 242 229 conn: Connection, 243 230 limit: Int, 244 231 ) -> Result(#(BitArray, List(#(String, String))), BodyError) { 245 - let Http1(transport:, socket:, self:, buffer:, framing:, ..) = conn 232 + let connection.Http1Connection( 233 + transport:, 234 + socket:, 235 + self:, 236 + buffer:, 237 + framing:, 238 + .., 239 + ) = conn 246 240 247 241 case framing, consume_body(transport, socket, buffer, framing, limit) { 248 242 _framing, Ok(#(body, trailers, leftover)) -> { ··· 275 269 max_chunk_bytes max_chunk_bytes: Int, 276 270 limit limit: Int, 277 271 ) -> Result(ChunkRead, BodyError) { 278 - let Http1(self:, buffer:, read:, chunk_remaining:, ..) = conn 272 + let connection.Http1Connection(self:, buffer:, read:, chunk_remaining:, ..) = 273 + conn 279 274 280 275 case pull_chunk(conn, max_chunk_bytes, limit) { 281 276 Ok(PulledChunk(data, next)) -> { ··· 319 314 option.Some(connection.BodyDrained(leftover)) -> #(leftover, True) 320 315 option.Some(connection.BodyAbandoned) -> #(<<>>, False) 321 316 option.Some(connection.BodyProgress(buffer:, read:, chunk_remaining:)) -> 322 - drain_remaining(Http1(..conn, buffer:, read:, chunk_remaining:)) 317 + connection.Http1Connection(..conn, buffer:, read:, chunk_remaining:) 318 + |> drain_remaining 323 319 option.None -> drain_remaining(conn) 324 320 option.Some(connection.StreamFinished(..)) -> 325 321 panic as "drain_messages routes stream messages to the other slot" ··· 359 355 // Drains whatever's left of `conn`'s body, up to `auto_drain_limit` more bytes 360 356 // past however much has already been read. 361 357 fn drain_remaining(conn: Connection) -> #(BitArray, Bool) { 362 - let Http1(read:, ..) = conn 358 + let connection.Http1Connection(read:, ..) = conn 363 359 do_drain_remaining(conn, read + auto_drain_limit) 364 360 } 365 361 ··· 492 488 max_chunk_bytes: Int, 493 489 limit: Int, 494 490 ) -> Result(Pulled, BodyError) { 495 - let Http1(buffer:, framing:, read:, chunk_remaining:, ..) = conn 491 + let connection.Http1Connection(buffer:, framing:, read:, chunk_remaining:, ..) = 492 + conn 496 493 497 494 case framing { 498 495 connection.NoBody -> Ok(PulledDone([], buffer)) ··· 514 511 read: Int, 515 512 max_chunk_bytes: Int, 516 513 ) -> Result(Pulled, ParseError) { 517 - let Http1(transport:, socket:, buffer:, ..) = conn 514 + let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn 518 515 519 516 case length - read { 520 517 0 -> Ok(PulledDone([], buffer)) ··· 526 523 buffer, 527 524 want, 528 525 )) 529 - Ok(PulledChunk(data, Http1(..conn, buffer: leftover, read: read + want))) 526 + let conn = 527 + connection.Http1Connection(..conn, buffer: leftover, read: read + want) 528 + Ok(PulledChunk(data, conn)) 530 529 } 531 530 } 532 531 } ··· 541 540 chunk_remaining: Int, 542 541 max_chunk_bytes: Int, 543 542 ) -> Result(Pulled, ParseError) { 544 - let Http1(transport:, socket:, buffer:, ..) = conn 543 + let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn 545 544 546 545 case chunk_remaining { 547 546 0 -> { ··· 563 562 } 564 563 size if read + size > limit -> Error(ChunkTooLarge) 565 564 size -> 566 - take_chunk_slice(Http1(..conn, buffer:), read, size, max_chunk_bytes) 565 + connection.Http1Connection(..conn, buffer:) 566 + |> take_chunk_slice(read, size, max_chunk_bytes) 567 567 } 568 568 } 569 569 remaining -> take_chunk_slice(conn, read, remaining, max_chunk_bytes) ··· 576 576 chunk_remaining: Int, 577 577 max_chunk_bytes: Int, 578 578 ) -> Result(Pulled, ParseError) { 579 - let Http1(transport:, socket:, buffer:, ..) = conn 579 + let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn 580 580 581 581 let want = int.min(chunk_remaining, max_chunk_bytes) 582 582 let final_slice = want == chunk_remaining ··· 586 586 take_chunk_prefix(buffer, want, final_slice) 587 587 }) 588 588 589 - Ok(PulledChunk( 590 - data, 591 - Http1( 589 + let conn = 590 + connection.Http1Connection( 592 591 ..conn, 593 592 buffer:, 594 593 read: read + want, 595 594 chunk_remaining: chunk_remaining - want, 596 - ), 597 - )) 595 + ) 596 + Ok(PulledChunk(data, conn)) 598 597 } 599 598 600 599 // Runs `step` against `buffer`, pulling more bytes from the socket only when ··· 823 822 } 824 823 825 824 /// How a streamed response writes its body chunks to the wire. 826 - pub type ResponseWriter { 827 - ResponseWriter( 828 - transport: transport.Transport, 829 - socket: socket.Socket, 830 - self: process.Subject(connection.Http1Signal), 831 - // `True` on HTTP/1.1: frame each chunk with its hex size and a trailing 832 - // `0\r\n\r\n` terminator. `False` on HTTP/1.0, which has no chunked 833 - // encoding: write raw bytes and let the body end when the connection 834 - // closes. 835 - chunked: Bool, 836 - // Whether the connection should stay alive once the stream finishes. 837 - // Always `False` when `chunked` is `False`. 838 - keep_alive: Bool, 839 - ) 840 - } 841 - 842 - // ResponseWriter and connection.ResponseWriter are structurally identical. 843 - @external(erlang, "gleam_stdlib", "identity") 844 - pub fn unsafe_to_http1_writer( 845 - writer: connection.ResponseWriter, 846 - ) -> ResponseWriter 825 + pub type ResponseWriter = 826 + connection.Http1ResponseWriter 847 827 848 828 fn chunk_frame(chunk: BitArray) -> bytes_tree.BytesTree { 849 829 bytes_tree.new() ··· 1737 1717 fn bit_array_to_string(bits: BitArray) -> Result(String, Nil) 1738 1718 1739 1719 // Used only for bytes already proven valid UTF-8 elsewhere!! 1740 - @external(erlang, "gleam_stdlib", "identity") 1720 + @external(erlang, "ewe_ffi", "identity") 1741 1721 fn unsafe_to_string(bits: BitArray) -> String