To manage a connection within a long-running process, you can wrap a Mint connection in a GenServer. This pattern is useful for maintaining a persistent connection to a host or managing multiple connections within a single process.
Implementation Details:
- Initialization: Call
Mint.HTTP.connect(scheme, host, port) in the init/1 callback to create the initial connection. - Request Handling: In
handle_call/3, use Mint.HTTP.request/5. Since the connection is immutable, you must update the conn in your GenServer state with the returned connection. Store the request_ref and the caller's from pid to facilitate asynchronous replies. - Response Processing: Use
handle_info/2 to receive messages from the socket. Pass these messages to Mint.HTTP.stream/2. - Asynchronous Reply: As
Mint.HTTP.stream/2 parses messages (status, headers, data, and :done), update your state. When the :done message for a specific request_ref is received, use GenServer.reply(from, {:ok, response}) to unblock the original caller.
Concurrency Note:
- HTTP/1: Requests will be pipelined (sent sequentially without waiting for responses).
- HTTP/2: Requests will be truly concurrent.
- To avoid pipelining in HTTP/1, you must manually queue or reject requests if one is already in progress.
defmodule ConnectionProcess do
use GenServer
require Logger
defstruct [:conn, requests: %{}]
def start_link({scheme, host, port}) do
GenServer.start_link(__MODULE__, {scheme, host, port})
end
def request(pid, method, path, headers, body) do
GenServer.call(pid, {:request, method, path, headers, body})
end
@impl true
def init({scheme, host, port}) do
case Mint.HTTP.connect(scheme, host, port) do
{:ok, conn} ->
state = %__MODULE__{conn: conn}
{:ok, state}
{:error, reason} ->
{:stop, reason}
end
end
@impl true
def handle_call({:request, method, path, headers, body}, from, state) do
case Mint.HTTP.request(state.conn, method, path, headers, body) do
{:ok, conn, request_ref} ->
state = put_in(state.conn, conn)
state = put_in(state.requests[request_ref], %{from: from, response: %{}})
{:noreply, state}
{:error, conn, reason} ->
state = put_in(state.conn, conn)
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_info(message, state) do
case Mint.HTTP.stream(state.conn, message) do
:unknown ->
_ = Logger.error(fn -> "Received unknown message: " <> inspect(message) end)
{:noreply, state}
{:ok, conn, responses} ->
state = put_in(state.conn, conn)
state = Enum.reduce(responses, state, &process_response/2)
{:noreply, state}
end
end
defp process_response({:status, request_ref, status}, state) do
put_in(state.requests[request_ref].response[:status], status)
end
defp process_response({:headers, request_ref, headers}, state) do
put_in(state.requests[request_ref].response[:headers], headers)
end
defp process_response({:data, request_ref, new_data}, state) do
update_in(state.requests[request_ref].response[:data], fn data -> (data || "") <> new_data end)
end
defp process_response({:done, request_ref}, state) do
{%{response: response, from: from}, state} = pop_in(state.requests[request_ref])
GenServer.reply(from, {:ok, response})
state
end
end