Skip to content

Message Flow

Gluegun keeps the asynchronous process model of Gun visible. It does not hide the model behind one blocking request API. This is why the lower-level gluegun/request, gluegun/message, and gluegun/websocket modules are useful for streams, push, upgrades, and long-lived connections.

The main flow is:

  1. Open a connection with gluegun/connection.
  2. Wait until the connection is ready with connection.await_up.
  3. Start a stream with gluegun/request, or a WebSocket upgrade with gluegun/websocket.
  4. Wait for typed gluegun/message.Message values for that stream.
  5. Decide if you continue the stream, add flow-control allowance, cancel, or close the connection.

The high-level gluegun/client functions use this flow internally for usual request and response work. They collect the full response in memory by design. They reject protocol messages such as push, upgrades, and WebSocket frames.

  • message.await(conn, stream, timeout) returns the next message for a stream as a typed Message. Use it in a loop to process Inform, Response, Data, Trailers, Push, Upgrade, and WebSocket values explicitly. This is the correct option for streams, server push, upgrade flows, or when you apply backpressure.
  • message.await_body(conn, stream, timeout) collects body chunks until Fin and returns the bytes as one value. Call it only after you receive the Response message (for example, from a previous await). The full body stays in memory. If the response ends with trailers, await_body still returns the body, but it discards the trailer headers. Use await when you must get the trailers.
  • message.decode(gun_message) changes a raw Gun mailbox message into a typed Envelope(connection:, stream:, message:). The envelope contains the decoded Message and its connection and stream. It accepts the mailbox tuples that Gun sends to the process that owns the stream (gun_response, gun_data, gun_inform, gun_trailers, gun_push, gun_upgrade, gun_ws). It rejects the shorter terms that gun:await/3 returns with DecodeError, because those terms have no connection or stream identity. Use decode when your own Erlang FFI receives Gun messages outside the Gluegun functions. If your process owns several streams, use the envelope to identify the source of each message. For Push, the envelope's stream is the existing stream that received the notification. The new stream is inside message.Push.

For chunked bodies, see the streaming guide. For WebSocket upgrade flows, see the websockets guide.

Gun open/2 returns immediately after it starts the connection process. The socket connect and the protocol negotiation occur asynchronously. connection.await_up waits for the Gun up message and returns the negotiated Protocol.

The returned Protocol is the protocol that Gun selected. For TLS connections, this includes the ALPN result. If you do not call await_up, later stream messages still queue correctly and the connection can still work. But your code cannot know if Gun negotiated HTTP/1.1 or HTTP/2. This is important for decisions such as websocket.upgrade_with_protocol, which rejects Http2 before it calls Gun.

connection.close calls Gun's supervisor to stop the connection process. The supervisor waits for exit, then forces termination after its shutdown timeout (five seconds in Gun 2.1/2.2). This is not an HTTP stream drain. The call does not flush your mailbox.

connection.shutdown sends an asynchronous graceful-shutdown request. Gun can finish active streams before it exits. The call does not wait for exit and cannot interrupt a blocked setup stage.

Use connection.terminate when a deadline expires or setup stalls. It sends an untrappable kill signal, waits for process exit, then flushes that connection's Gun messages from the calling process. Call it from the receiving process. After it returns, the Gun process cannot send more messages. Other receivers and third-party message forwarders need their own cleanup. Forced termination can produce a Gun supervisor report with reason killed.

Gluegun shows the half-close model of Gun with gluegun/fin.Fin and gluegun/fin.NoFin.

On the request side, request.data(conn, stream, fin.NoFin, chunk) sends more bytes and tells Gun that the request body is not complete. fin.Fin on the last chunk tells Gun that there are no more request bytes. This final signal lets many servers start the response.

On the response side, message.Response(fin, status, headers) tells you if the final response has only headers (fin.Fin) or if body data follows (fin.NoFin). Later message.Data(fin, data) chunks use the same signal. fin.Fin means that the server has sent the full body. fin.NoFin means that more data follows. Response trailers, when present, arrive separately as message.Trailers.

request.cancel(conn, stream) sends an asynchronous cancellation for one stream. Queued messages and late arrivals can remain while Gun processes it. The connection stays open for other streams.

request.flush(conn) removes only messages queued at that instant. For terminal cleanup, cancel the stream if needed, then call connection.terminate(conn) from the receiver. Termination discards all streams on the connection.

Set finite credit with request.with_initial_flow(options, credit) before request.request or request.start_stream. Without it, Gun starts with unlimited credit; a later update_flow cannot change unlimited credit to finite credit.

With Gun's default data handler, one credit permits one data message in both HTTP/1.1 and HTTP/2. Headers do not consume credit. request.update_flow(conn, stream, increment) adds credit in the same unit. HTTP/1.1 pauses socket reads when credit runs out. HTTP/2 stops replenishing the stream's byte window; it can still process frames already in that window or socket read. Credit is not the HTTP/2 byte window and does not impose a strict message-count or memory bound. See the streaming guide for allocation limits.

message.WebSocket(frame) holds typed WebSocket frames. Gluegun keeps text, binary, ping, pong, and close frames as different variants. It does not merge them into one shape.

Gun processes ping and pong automatically by default. websocket.with_silence_pings controls if automatic ping processing stays hidden, or if ping frames are visible to your code. Ping and Pong each hold a payload BitArray. An empty payload is valid. It means that no application data was attached.

message.CloseWithReason(code, reason) keeps the close reason as raw bytes. RFC 6455 says that the reason payload is UTF-8 text. Gluegun keeps it as a BitArray. Thus the caller decides when and how to decode it safely.

Gun sends asynchronous connection and stream messages to the Erlang process that called gun:open. To send WebSocket upgrade and frame messages to a different process, use websocket/raw.with_reply_to. It sets the Gun reply_to option to that process identifier.

If the receiving process stops before it processes the messages, those messages are lost. Gluegun does not keep them in a different location.

import gleam/erlang/process
import gluegun/connection
import gluegun/websocket
import gluegun/websocket/raw
pub fn upgrade_in_current_process(conn) {
let options =
websocket.upgrade_options()
|> raw.with_reply_to(reply_to: process.self())
let assert Ok(timeout) = connection.milliseconds(5000)
let assert Ok(protocol) = connection.await_up(conn, timeout)
let assert Ok(stream) =
websocket.upgrade_with_protocol_and_options(
conn,
protocol,
"/ws",
[],
options,
)
stream
}

See the message reference, request reference, and WebSocket guide for the full API around these concepts.