From 87dde5d0fac11b64e91812bcecb1b408c4f43f0c Mon Sep 17 00:00:00 2001 From: vshakitskiy Date: Wed, 5 Aug 2026 14:33:55 +0300 Subject: [PATCH] ensure handlers are safe from sudden crushes --- CHANGELOG.md | 8 +-- dev/autobahn.gleam | 18 ++++--- dev/serve.gleam | 3 +- src/ewe.gleam | 69 ++++++++++++++++++++------ src/ewe/internal/http1.gleam | 20 ++++---- src/ewe/internal/http1/encoder.gleam | 55 +++++++++++++------- src/ewe/internal/http1/sse.gleam | 47 +++++++++++++----- src/ewe/internal/http1/websocket.gleam | 58 +++++++++++++++------- src/ewe/internal/rescue.gleam | 2 + src/ewe/internal/stream.gleam | 16 ------ src/ewe/internal/stream_ffi.erl | 18 ------- test/ewe/internal/stream_test.gleam | 41 --------------- test/ewe/internal/stream_test_ffi.erl | 11 ---- 13 files changed, 192 insertions(+), 174 deletions(-) create mode 100644 src/ewe/internal/rescue.gleam delete mode 100644 src/ewe/internal/stream.gleam delete mode 100644 src/ewe/internal/stream_ffi.erl delete mode 100644 test/ewe/internal/stream_test.gleam delete mode 100644 test/ewe/internal/stream_test_ffi.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index 8164285..00f26d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,12 +33,8 @@ and no init/loop callback for response streaming anymore. - Response streaming, server-sent events and websockets no longer spawn a process per response, they run in the connection process itself. -- `send_chunk`, `send_event`, `send_text_frame` and `send_binary_frame` no - longer return a `Result`. Writing to a client that has gone now ends the - handler where it stands rather than letting it carry on producing a body with - nowhere to go. A stream that ends this way is not an error and is not reported - as one while a handler that crashes for its own reasons still does. `on_close` - runs either way. +- In replacement of the `glisten` socket reason the send functions used to fail + with there is now `SendError`. - Rename `SSEConnection`, `SSEEvent` and `SSENext` to `SseConnection`, `SseEvent` and `SseNext`. - Add `comment` for the server-sent events comment that keeps an idle stream diff --git a/dev/autobahn.gleam b/dev/autobahn.gleam index 9411b75..b10db4b 100644 --- a/dev/autobahn.gleam +++ b/dev/autobahn.gleam @@ -39,14 +39,16 @@ fn echo_message( message: ewe.WebsocketMessage(Nil), ) -> ewe.WebsocketNext(Nil, Nil) { case message { - ewe.TextFrame(text) -> { - ewe.send_text_frame(conn, text) - ewe.websocket_continue(state) - } - ewe.BinaryFrame(data) -> { - ewe.send_binary_frame(conn, data) - ewe.websocket_continue(state) - } + ewe.TextFrame(text) -> + case ewe.send_text_frame(conn, text) { + Ok(Nil) -> ewe.websocket_continue(state) + Error(_send) -> ewe.websocket_stop() + } + ewe.BinaryFrame(data) -> + case ewe.send_binary_frame(conn, data) { + Ok(Nil) -> ewe.websocket_continue(state) + Error(_send) -> ewe.websocket_stop() + } ewe.UserMessage(_message) -> ewe.websocket_continue(state) } } diff --git a/dev/serve.gleam b/dev/serve.gleam index 97b8a7c..0654fb4 100644 --- a/dev/serve.gleam +++ b/dev/serve.gleam @@ -5,6 +5,7 @@ import gleam/http import gleam/http/request import gleam/http/response import gleam/option +import gleam/result import logging pub fn main() -> Nil { @@ -38,7 +39,7 @@ fn handle_request( http.Post, "/echo/chunked" -> echo_chunked(request, bytes_tree.new()) http.Get, "/stream" -> { use writer <- ewe.stream_response(response.new(200)) - let writer = ewe.send_chunk(writer, <<"hello, ":utf8>>) + use writer <- result.try(ewe.send_chunk(writer, <<"hello, ":utf8>>)) ewe.finish_chunk(writer, <<"Joe!":utf8>>) } http.Get, "/file/small" -> { diff --git a/src/ewe.gleam b/src/ewe.gleam index 0b5936e..19e307e 100644 --- a/src/ewe.gleam +++ b/src/ewe.gleam @@ -582,6 +582,16 @@ pub fn read_body_chunk( } } +// TODO: obviously not the string reason variant but this is for later! +/// Why a write to the client did not go through. +pub type SendError { + SendError(reason: String) +} + +fn to_send_error(reason: socket.SocketReason) -> SendError { + SendError(socket.reason_to_string(reason)) +} + /// A handle for writing a streamed response's body, obtained from /// `stream_response`. pub type ResponseWriter = @@ -596,35 +606,52 @@ pub type ResponseWriter = /// on the process ending rather than in code after the write. pub fn stream_response( response: response.Response(a), - handler: fn(ResponseWriter) -> Nil, + handler: fn(ResponseWriter) -> Result(Nil, SendError), ) -> response.Response(Body) { - response.set_body(response, Streaming(connection.StreamingMetadata(handler))) + // What the handler was left holding when a write failed is its own business, + // ewe already learns whether the stream finished from the writer. + let stream = fn(writer) { + let _sent = handler(writer) + Nil + } + + response.set_body(response, Streaming(connection.StreamingMetadata(stream))) } /// Sends one response body chunk, threading the writer through so it can be /// piped. For the last chunk use `finish_chunk` instead, it closes the /// stream in the same round trip. -pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { +pub fn send_chunk( + writer: ResponseWriter, + chunk: BitArray, +) -> Result(ResponseWriter, SendError) { case writer { connection.Http1Writer(writer) -> - connection.Http1Writer(encoder.send_chunk(writer, chunk)) + encoder.send_chunk(writer, chunk) + |> result.map(connection.Http1Writer) + |> result.map_error(to_send_error) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } /// Sends `chunk` as the final response body chunk and closes the stream. -pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { +pub fn finish_chunk( + writer: ResponseWriter, + chunk: BitArray, +) -> Result(Nil, SendError) { case writer { - connection.Http1Writer(writer) -> encoder.finish_chunk(writer, chunk) + connection.Http1Writer(writer) -> + encoder.finish_chunk(writer, chunk) |> result.map_error(to_send_error) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } /// Closes the stream with no further data. Use `finish_chunk` instead if /// there's one last chunk to send. -pub fn finish_response(writer: ResponseWriter) -> Nil { +pub fn finish_response(writer: ResponseWriter) -> Result(Nil, SendError) { case writer { - connection.Http1Writer(writer) -> encoder.finish_response(writer) + connection.Http1Writer(writer) -> + encoder.finish_response(writer) |> result.map_error(to_send_error) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } @@ -690,9 +717,13 @@ pub fn event_retry(event: SseEvent, retry: Int) -> SseEvent { /// Sends event to the client. If the client has gone the stream ends here: /// `on_close` runs and the handler is not called again. -pub fn send_event(conn: SseConnection, event: SseEvent) -> Nil { +pub fn send_event( + conn: SseConnection, + event: SseEvent, +) -> Result(Nil, SendError) { case conn { - connection.Http1Sse(conn) -> http1_sse.send(conn, event) + connection.Http1Sse(conn) -> + http1_sse.send(conn, event) |> result.map_error(to_send_error) connection.Http2Sse -> todo as "HTTP/2 is not implemented yet!" } } @@ -860,17 +891,25 @@ fn to_internal_close_code(code: CloseCode) -> websocks.CloseCode { } /// Sends a text frame. If the client has gone the WebSocket ends here. -pub fn send_text_frame(conn: WebsocketConnection, text: String) -> Nil { +pub fn send_text_frame( + conn: WebsocketConnection, + text: String, +) -> Result(Nil, SendError) { case conn { - connection.Http1Websocket(conn) -> http1_websocket.send_text(conn, text) + connection.Http1Websocket(conn) -> + http1_websocket.send_text(conn, text) |> result.map_error(to_send_error) connection.Http2Websocket -> todo as "HTTP/2 is not implemented yet!" } } /// Sends a binary frame. If the client has gone the WebSocket ends here. -pub fn send_binary_frame(conn: WebsocketConnection, data: BitArray) -> Nil { +pub fn send_binary_frame( + conn: WebsocketConnection, + data: BitArray, +) -> Result(Nil, SendError) { case conn { - connection.Http1Websocket(conn) -> http1_websocket.send_binary(conn, data) + connection.Http1Websocket(conn) -> + http1_websocket.send_binary(conn, data) |> result.map_error(to_send_error) connection.Http2Websocket -> todo as "HTTP/2 is not implemented yet!" } } @@ -881,7 +920,7 @@ pub fn send_close_frame( conn: WebsocketConnection, reason: CloseReason, ) -> WebsocketNext(user_state, user_message) { - case conn { + let _sent = case conn { connection.Http1Websocket(conn) -> http1_websocket.send_close(conn, to_internal_close_reason(reason)) connection.Http2Websocket -> todo as "HTTP/2 is not implemented yet!" diff --git a/src/ewe/internal/http1.gleam b/src/ewe/internal/http1.gleam index c0f6068..6e870c9 100644 --- a/src/ewe/internal/http1.gleam +++ b/src/ewe/internal/http1.gleam @@ -4,7 +4,7 @@ import ewe/internal/http1/body import ewe/internal/http1/connection as http1 import ewe/internal/http1/encoder import ewe/internal/http1/parser -import ewe/internal/stream +import ewe/internal/rescue import gleam/bytes_tree import gleam/erlang/process import gleam/http @@ -64,7 +64,7 @@ pub fn handle_message( let request = to_request(head, connection, body_connection) - case rescue_handler(fn() { state.handler(request) }) { + case rescue.handler(fn() { state.handler(request) }) { Error(details) -> crashed(connection, details) Ok(response) -> { let drained = drain_messages(self) @@ -244,10 +244,15 @@ fn send_response( keep_alive:, )) - case stream.rescue_dead(fn() { stream_handler(writer) }) { - // The client went away mid stream. There is nothing to terminate the - // body with and nothing to reuse. - Error(_reason) -> Ok(SentClose) + case rescue.handler(fn() { stream_handler(writer) }) { + // The head is already on the wire so no other answer can be given. + Error(details) -> { + logging.log( + logging.Error, + "Caught a crash in the streaming handler: " <> details, + ) + Ok(SentClose) + } Ok(Nil) -> { let drained = drain_messages(self) case drained.stream { @@ -383,6 +388,3 @@ fn do_drain_remaining( Error(_reason) -> ResolvedBody(<<>>, http1.CloseAfterResponse) } } - -@external(erlang, "ewe_ffi", "rescue_handler") -fn rescue_handler(handler: fn() -> a) -> Result(a, String) diff --git a/src/ewe/internal/http1/encoder.gleam b/src/ewe/internal/http1/encoder.gleam index eefbb83..caaa9cc 100644 --- a/src/ewe/internal/http1/encoder.gleam +++ b/src/ewe/internal/http1/encoder.gleam @@ -3,7 +3,6 @@ import ewe/internal/connection import ewe/internal/file import ewe/internal/http1/connection as http1 import ewe/internal/http1/parser -import ewe/internal/stream import gleam/bit_array import gleam/bytes_tree import gleam/erlang/process @@ -291,14 +290,19 @@ pub fn frame( } } -pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { +pub fn send_chunk( + writer: ResponseWriter, + chunk: BitArray, +) -> Result(ResponseWriter, socket.SocketReason) { frame(bytes_tree.from_bit_array(chunk), writer.framing) |> write(writer, _) - - writer + |> result.replace(writer) } -pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { +pub fn finish_chunk( + writer: ResponseWriter, + chunk: BitArray, +) -> Result(Nil, socket.SocketReason) { // The terminator rides along with the last chunk to save a write. let bytes = case writer.framing { http1.ChunkedStream -> @@ -308,22 +312,22 @@ pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { ) http1.CloseDelimitedStream -> bytes_tree.from_bit_array(chunk) } - write(writer, bytes) - finish(writer) + + write(writer, bytes) |> finished(writer, _) } -pub fn finish_response(writer: ResponseWriter) -> Nil { - case end_stream(writer.transport, writer.socket, writer.framing) { - Ok(Nil) -> finish(writer) - Error(reason) -> stream.dead(reason) - } +pub fn finish_response( + writer: ResponseWriter, +) -> Result(Nil, socket.SocketReason) { + end_stream(writer.transport, writer.socket, writer.framing) + |> finished(writer, _) } -fn write(writer: ResponseWriter, bytes: bytes_tree.BytesTree) -> Nil { - case transport.send(writer.transport, writer.socket, bytes) { - Ok(Nil) -> Nil - Error(reason) -> stream.dead(reason) - } +fn write( + writer: ResponseWriter, + bytes: bytes_tree.BytesTree, +) -> Result(Nil, socket.SocketReason) { + transport.send(writer.transport, writer.socket, bytes) } pub fn end_stream( @@ -338,10 +342,23 @@ pub fn end_stream( } } -fn finish(writer: ResponseWriter) -> Nil { - http1.StreamFinished(keep_alive: writer.keep_alive) +/// The stream is over either way so the connection process is told so even +/// when the last write never landed. One that ended on a failed write leaves +/// nothing to hand back. +fn finished( + writer: ResponseWriter, + sent: Result(Nil, socket.SocketReason), +) -> Result(Nil, socket.SocketReason) { + let keep_alive = case sent { + Ok(Nil) -> writer.keep_alive + Error(_reason) -> http1.CloseAfterResponse + } + + http1.StreamFinished(keep_alive:) |> http1.StreamSignal |> process.send(writer.self, _) + + sent } /// Which headers the encoder writes itself for a body, and so drops from the diff --git a/src/ewe/internal/http1/sse.gleam b/src/ewe/internal/http1/sse.gleam index 8fedb6e..92e73f6 100644 --- a/src/ewe/internal/http1/sse.gleam +++ b/src/ewe/internal/http1/sse.gleam @@ -1,14 +1,15 @@ import ewe/internal/connection import ewe/internal/http1/connection as http1 import ewe/internal/http1/encoder +import ewe/internal/rescue import ewe/internal/sse -import ewe/internal/stream import gleam/dynamic import gleam/erlang/atom import gleam/erlang/process import glisten/socket import glisten/socket/options import glisten/transport +import logging /// Runs a Server-Sent Events stream, reporting through `conn.self` whether the /// connection can carry another request afterwards. @@ -40,7 +41,10 @@ fn ended( keep_alive: http1.KeepAlive, outcome: connection.Outcome, ) -> connection.Outcome { - let _dead = stream.rescue_dead(fn() { on_close(handle, state) }) + // A bug in `on_close` is still a bug but it must not take the connection + // down on the way out of a stream that has already ended. + // TODO: log for a user? + let _crashed = rescue.handler(fn() { on_close(handle, state) }) finished(conn, keep_alive) outcome } @@ -62,6 +66,25 @@ fn dropped( ) } +/// A handler that crashed cannot be asked what to do next so the stream is +/// ended for it and the connection given up rather than the crash taking the +/// whole process with it. +fn crashed( + conn: http1.SseConnection, + handle: connection.SseConnection, + state: user_state, + on_close: fn(connection.SseConnection, user_state) -> Nil, + details: String, +) -> connection.Outcome { + logging.log( + logging.Error, + "Caught a crash in the server-sent events handler: " <> details, + ) + + connection.StoppedAbnormal("the handler crashed") + |> ended(conn, handle, state, on_close, http1.CloseAfterResponse, _) +} + /// The socket gave out, which ends the stream whatever it was doing. fn socket_failed( conn: http1.SseConnection, @@ -106,10 +129,8 @@ fn loop( connection.StoppedAbnormal(reason) |> ended(conn, handle, state, on_close, http1.CloseAfterResponse, _) Message(message) -> - // A send inside the handler can find the client gone before the socket - // has told us, so both routes out land on the same teardown. - case stream.rescue_dead(fn() { step(handle, state, message) }) { - Error(_reason) -> dropped(conn, handle, state, on_close) + case rescue.handler(fn() { step(handle, state, message) }) { + Error(details) -> crashed(conn, handle, state, on_close, details) Ok(sse.Proceed(state)) -> loop(conn, handle, selector, state, reuse, step, on_close) Ok(sse.Halt(outcome)) -> @@ -141,13 +162,13 @@ fn finished(conn: http1.SseConnection, keep_alive: http1.KeepAlive) -> Nil { |> process.send(conn.self, _) } -pub fn send(conn: http1.SseConnection, event: sse.Event) -> Nil { - let bytes = sse.encode(event) |> encoder.frame(conn.framing) - - case transport.send(conn.transport, conn.socket, bytes) { - Ok(Nil) -> Nil - Error(reason) -> stream.dead(reason) - } +pub fn send( + conn: http1.SseConnection, + event: sse.Event, +) -> Result(Nil, socket.SocketReason) { + sse.encode(event) + |> encoder.frame(conn.framing) + |> transport.send(conn.transport, conn.socket, _) } /// glisten rearms `{active, once}` only once its loop callback returns and an diff --git a/src/ewe/internal/http1/websocket.gleam b/src/ewe/internal/http1/websocket.gleam index 1921f3f..ab771ea 100644 --- a/src/ewe/internal/http1/websocket.gleam +++ b/src/ewe/internal/http1/websocket.gleam @@ -1,6 +1,6 @@ import ewe/internal/connection import ewe/internal/http1/connection as http1 -import ewe/internal/stream +import ewe/internal/rescue import ewe/internal/websocket import gleam/bytes_tree import gleam/dynamic @@ -12,6 +12,7 @@ import gleam/result import glisten/socket import glisten/socket/options import glisten/transport +import logging import websocks pub type HandshakeError { @@ -107,7 +108,10 @@ fn ended( outcome: connection.Outcome, ) -> connection.Outcome { let handle = connection.Http1Websocket(conn) - let _dead = stream.rescue_dead(fn() { on_close(handle, state) }) + // A bug in `on_close` is still a bug but it must not take the connection + // down on the way out of a socket that has already ended. + // TODO: log for the user? + let _crashed = rescue.handler(fn() { on_close(handle, state) }) websocks.close_context(conn.context) outcome @@ -121,6 +125,27 @@ fn stopped( ended(conn, state, on_close, connection.Stopped) } +/// A handler that crashed cannot be asked what to do next so the socket is +/// ended for it rather than the crash taking the whole process with it. +fn crashed( + conn: http1.WebsocketConnection, + state: user_state, + on_close: fn(connection.WebsocketConnection, user_state) -> Nil, + details: String, +) -> connection.Outcome { + logging.log( + logging.Error, + "Caught a crash in the websocket handler: " <> details, + ) + + ended( + conn, + state, + on_close, + connection.StoppedAbnormal("the handler crashed"), + ) +} + /// The socket gave out which ends the connection whatever it was doing. fn socket_failed( conn: http1.WebsocketConnection, @@ -241,8 +266,8 @@ fn deliver( ) -> connection.Outcome { let handle = connection.Http1Websocket(conn) - case stream.rescue_dead(fn() { step(handle, state, message) }) { - Error(_reason) -> stopped(conn, state, on_close) + case rescue.handler(fn() { step(handle, state, message) }) { + Error(details) -> crashed(conn, state, on_close, details) Ok(websocket.Proceed(user_state: state, messages:)) -> { let selector = case messages { option.Some(messages) -> merge_socket_selector(messages) @@ -272,30 +297,36 @@ fn resolve( } } -pub fn send_text(conn: http1.WebsocketConnection, text: String) -> Nil { +pub fn send_text( + conn: http1.WebsocketConnection, + text: String, +) -> Result(Nil, socket.SocketReason) { websocks.encode_text_frame( payload: bit_array_from_string(text), context: conn.context, masking: option.None, ) - |> write_or_die(conn, _) + |> write(conn, _) } -pub fn send_binary(conn: http1.WebsocketConnection, data: BitArray) -> Nil { +pub fn send_binary( + conn: http1.WebsocketConnection, + data: BitArray, +) -> Result(Nil, socket.SocketReason) { websocks.encode_binary_frame( payload: data, context: conn.context, masking: option.None, ) - |> write_or_die(conn, _) + |> write(conn, _) } pub fn send_close( conn: http1.WebsocketConnection, reason: websocks.CloseReason, -) -> Nil { +) -> Result(Nil, socket.SocketReason) { websocks.encode_close_frame(reason:, masking: option.None) - |> write_or_die(conn, _) + |> write(conn, _) } fn with_context( @@ -320,13 +351,6 @@ fn write( |> transport.send(conn.transport, conn.socket, _) } -fn write_or_die(conn: http1.WebsocketConnection, frame: BitArray) -> Nil { - case write(conn, frame) { - Ok(Nil) -> Nil - Error(reason) -> stream.dead(reason) - } -} - /// glisten rearms the socket only once its loop callback returns and a socket /// does not return until it is over, so the frames have to be asked for here. fn activate( diff --git a/src/ewe/internal/rescue.gleam b/src/ewe/internal/rescue.gleam new file mode 100644 index 0000000..e27336d --- /dev/null +++ b/src/ewe/internal/rescue.gleam @@ -0,0 +1,2 @@ +@external(erlang, "ewe_ffi", "rescue_handler") +pub fn handler(handler: fn() -> a) -> Result(a, String) diff --git a/src/ewe/internal/stream.gleam b/src/ewe/internal/stream.gleam deleted file mode 100644 index fca9ca9..0000000 --- a/src/ewe/internal/stream.gleam +++ /dev/null @@ -1,16 +0,0 @@ -import glisten/socket - -/// Ends the handler writing this stream, because there is no longer anywhere -/// for it to write to. Never returns. -/// -/// A handler is straight line code that ewe has handed control to, so unwinding -/// is the only way to stop it doing work for a client that has gone. It also -/// keeps the one thing a handler could do about a failed write out of its way, -/// since stopping is the only sane answer. -@external(erlang, "stream_ffi", "dead") -pub fn dead(reason: socket.SocketReason) -> a - -/// Runs `handler`, catching only the end that `dead` raises. Anything else a -/// handler raises is a bug and is left to crash. -@external(erlang, "stream_ffi", "rescue_dead") -pub fn rescue_dead(handler: fn() -> a) -> Result(a, socket.SocketReason) diff --git a/src/ewe/internal/stream_ffi.erl b/src/ewe/internal/stream_ffi.erl deleted file mode 100644 index 0794ce8..0000000 --- a/src/ewe/internal/stream_ffi.erl +++ /dev/null @@ -1,18 +0,0 @@ --module(stream_ffi). - --export([dead/1, rescue_dead/1]). - --define(STREAM_DEAD, ewe_stream_dead). - -dead(Reason) -> - erlang:error({?STREAM_DEAD, Reason}). - -%% Matches the sentinel exactly and reraises everything else with its own -%% stacktrace so a bug in a handler still crashes as a bug. -rescue_dead(Func) -> - try - {ok, Func()} - catch - error:{?STREAM_DEAD, Reason} -> {error, Reason}; - Class:Reason:Stacktrace -> erlang:raise(Class, Reason, Stacktrace) - end. diff --git a/test/ewe/internal/stream_test.gleam b/test/ewe/internal/stream_test.gleam deleted file mode 100644 index 25fdef3..0000000 --- a/test/ewe/internal/stream_test.gleam +++ /dev/null @@ -1,41 +0,0 @@ -import ewe/internal/stream -import gleam/erlang/process -import glisten/socket - -pub fn handler_running_to_completion_is_returned_test() { - assert stream.rescue_dead(fn() { 42 }) == Ok(42) -} - -pub fn dead_stream_unwinds_to_the_rescue_test() { - let handler = fn() { - stream.dead(socket.Closed) - panic as "a dead stream must not return to its handler" - } - - assert stream.rescue_dead(handler) == Error(socket.Closed) -} - -pub fn work_after_a_dead_stream_does_not_run_test() { - let subject = process.new_subject() - - let handler = fn() { - stream.dead(socket.Closed) - process.send(subject, "kept working") - } - - let _dead = stream.rescue_dead(handler) - - assert process.receive(subject, 0) == Error(Nil) - as "unwinding is what stops a handler producing a body nobody will read" -} - -pub fn a_handler_bug_is_not_mistaken_for_a_dead_stream_test() { - let crashed = - rescue(fn() { stream.rescue_dead(fn() { panic as "bug in the handler" }) }) - - assert crashed == Error(Nil) - as "swallowing a bug would report it as a client that went away" -} - -@external(erlang, "stream_test_ffi", "rescue") -fn rescue(handler: fn() -> a) -> Result(a, Nil) diff --git a/test/ewe/internal/stream_test_ffi.erl b/test/ewe/internal/stream_test_ffi.erl deleted file mode 100644 index fe3e322..0000000 --- a/test/ewe/internal/stream_test_ffi.erl +++ /dev/null @@ -1,11 +0,0 @@ --module(stream_test_ffi). - --export([rescue/1]). - -%% Catches every class, so a test can assert that something crashed at all. -rescue(Func) -> - try - {ok, Func()} - catch - _Class:_Reason -> {error, nil} - end. -- 2.51.2