Packages
hl7v2
3.7.0
3.10.1
3.9.0
3.8.0
3.7.0
3.6.0
3.5.0
3.4.0
3.3.6
3.3.5
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.0
3.1.1
3.1.0
3.0.2
3.0.1
3.0.0
2.11.0
2.10.0
2.9.1
2.9.0
2.8.2
2.8.1
2.8.0
2.7.1
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.3
2.1.2
2.1.1
2.1.0
1.4.6
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.6.0
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.1.0
Pure Elixir HL7 v2.x toolkit — schema-driven parsing, typed segments, message builder, MLLP transport
Current section
Files
Jump to
Current section
Files
lib/hl7v2/mllp/client.ex
defmodule HL7v2.MLLP.Client do
@moduledoc """
MLLP TCP client for sending HL7v2 messages.
Maintains a persistent TCP connection and provides synchronous
send-and-receive for HL7v2 messages over MLLP.
## Examples
{:ok, client} = HL7v2.MLLP.Client.start_link(host: "localhost", port: 2575)
{:ok, ack} = HL7v2.MLLP.Client.send_message(client, hl7_message)
:ok = HL7v2.MLLP.Client.close(client)
## TLS
{:ok, client} = HL7v2.MLLP.Client.start_link(
host: "remote.host",
port: 2576,
tls: [verify: :verify_peer, cacertfile: "ca.pem"]
)
"""
use GenServer
@default_timeout 30_000
@default_max_message_size 10_485_760
@doc """
Starts a client connection.
## Options
- `:host` (required) — hostname or IP address to connect to.
- `:port` (required) — TCP port to connect to.
- `:timeout` — send/receive timeout in milliseconds (default: `30_000`).
- `:max_message_size` — maximum response size in bytes (default: `10_485_760` — 10 MB).
Returns `{:error, :message_too_large}` if the response buffer exceeds this limit.
- `:tls` — keyword list of `:ssl` options. When present, connects via TLS.
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
@doc """
Sends an HL7v2 message and waits for the response.
MLLP is strictly request/response — one message sent, one ACK received.
Terminal errors that close the connection and stop the client:
- `{:error, :protocol_desync}` — stale bytes from a previous exchange
- `{:error, :message_too_large}` — response exceeded `:max_message_size`
After either error the caller must start a new client to continue.
The message is MLLP-framed before sending. The response is returned
with MLLP framing stripped.
## Options
- `:timeout` — override the default timeout for this call.
"""
@spec send_message(GenServer.server(), binary(), keyword()) ::
{:ok, binary()} | {:error, term()}
def send_message(client, message, opts \\ []) do
timeout = Keyword.get(opts, :timeout, @default_timeout)
GenServer.call(client, {:send, message, timeout}, timeout + 5_000)
end
@doc """
Closes the client connection.
"""
@spec close(GenServer.server()) :: :ok
def close(client) do
GenServer.stop(client)
end
# -- Callbacks --
@impl GenServer
def init(opts) do
host = Keyword.fetch!(opts, :host)
port = Keyword.fetch!(opts, :port)
timeout = Keyword.get(opts, :timeout, @default_timeout)
max_message_size = Keyword.get(opts, :max_message_size, @default_max_message_size)
tls_opts = Keyword.get(opts, :tls)
host_charlist = host_to_charlist(host)
case connect(host_charlist, port, tls_opts, timeout) do
{:ok, socket, transport} ->
{:ok,
%{
socket: socket,
transport: transport,
timeout: timeout,
max_message_size: max_message_size,
buffer: <<>>
}}
{:error, reason} ->
{:stop, reason}
end
end
@impl GenServer
def handle_call({:send, message, timeout}, _from, state) do
%{socket: socket, transport: transport} = state
# In strict MLLP request/response, the buffer MUST be empty between
# exchanges. Any leftover bytes (complete frames or partial data)
# indicate a protocol violation by the peer.
if state.buffer != <<>> do
require Logger
Logger.error("MLLP protocol desync: #{byte_size(state.buffer)} stale byte(s) in buffer")
transport_close(transport, socket)
{:stop, :normal, {:error, :protocol_desync}, %{state | buffer: <<>>, socket: nil}}
else
framed = HL7v2.MLLP.frame(message)
case transport_send(transport, socket, framed) do
:ok ->
case recv_response(transport, socket, <<>>, timeout, state.max_message_size) do
{:ok, response, remaining} ->
{:reply, {:ok, response}, %{state | buffer: remaining}}
{:error, :message_too_large} ->
# Oversized response contaminates the socket — close and stop.
require Logger
Logger.error("MLLP response exceeded max_message_size, closing connection")
transport_close(transport, socket)
{:stop, :normal, {:error, :message_too_large}, %{state | buffer: <<>>, socket: nil}}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
end
@impl GenServer
def terminate(_reason, %{socket: nil}) do
:ok
end
def terminate(_reason, %{socket: socket, transport: transport}) do
transport_close(transport, socket)
:ok
end
def terminate(_reason, _state), do: :ok
# -- Private --
defp connect(host, port, nil, timeout) do
tcp_opts = [:binary, active: false, packet: :raw]
case :gen_tcp.connect(host, port, tcp_opts, timeout) do
{:ok, socket} -> {:ok, socket, :tcp}
{:error, reason} -> {:error, reason}
end
end
defp connect(host, port, tls_opts, timeout) do
tcp_opts = [:binary, active: false, packet: :raw]
with {:ok, tcp_socket} <- :gen_tcp.connect(host, port, tcp_opts, timeout) do
case :ssl.connect(tcp_socket, tls_opts, timeout) do
{:ok, ssl_socket} ->
{:ok, ssl_socket, :ssl}
{:error, reason} ->
:gen_tcp.close(tcp_socket)
{:error, reason}
end
end
end
defp transport_send(:tcp, socket, data), do: :gen_tcp.send(socket, data)
defp transport_send(:ssl, socket, data), do: :ssl.send(socket, data)
defp transport_close(:tcp, socket), do: :gen_tcp.close(socket)
defp transport_close(:ssl, socket), do: :ssl.close(socket)
defp recv_response(transport, socket, initial_buffer, timeout, max_message_size) do
# Check if the buffer already contains a complete message from a previous call
case HL7v2.MLLP.extract_messages(initial_buffer) do
{[message | rest], remaining} ->
{:ok, message, rebuffer(rest, remaining)}
_ ->
recv_loop(transport, socket, initial_buffer, timeout, max_message_size)
end
end
defp recv_loop(transport, socket, buffer, timeout, max_message_size) do
case transport_recv(transport, socket, 0, timeout) do
{:ok, data} ->
new_buffer = <<buffer::binary, data::binary>>
if byte_size(new_buffer) > max_message_size do
{:error, :message_too_large}
else
case HL7v2.MLLP.extract_messages(new_buffer) do
{[message | rest], remaining} ->
{:ok, message, rebuffer(rest, remaining)}
{[], _remaining} ->
recv_loop(transport, socket, new_buffer, timeout, max_message_size)
end
end
{:error, reason} ->
{:error, reason}
end
end
defp transport_recv(:tcp, socket, length, timeout) do
:gen_tcp.recv(socket, length, timeout)
end
defp transport_recv(:ssl, socket, length, timeout) do
:ssl.recv(socket, length, timeout)
end
defp host_to_charlist(host) when is_binary(host), do: String.to_charlist(host)
defp host_to_charlist(host) when is_list(host), do: host
defp host_to_charlist(host) when is_atom(host), do: Atom.to_charlist(host)
# Re-frame unconsumed extracted messages back into the raw buffer.
# Any leftover will trigger a :protocol_desync error on the next send.
defp rebuffer([], remaining), do: remaining
defp rebuffer(extra_messages, remaining) do
reframed = extra_messages |> Enum.map(&HL7v2.MLLP.frame/1) |> IO.iodata_to_binary()
<<reframed::binary, remaining::binary>>
end
end