Skip to the content.

MLLPong

MLLPong is an open-source mock server for testing HL7 v2 messages over MLLP (Minimal Lower Layer Protocol) — packed into a single binary under 1 MB. No runtime, no dependencies, no bloat.

It exposes three TCP endpoints with different behaviors:

Endpoint Default Port Behavior
ACK handler 2575 Always replies with AA (application accept)
Chaos handler 2576 Always replies with AR (application reject)
Smart handler 2577 Responds based on message type rules from a JSON config file

Installation

◉ Homebrew (macOS / Linux)

brew install novalagung/tap/mllpong

◉ Binary (macOS / Linux)

curl -sSfL https://raw.githubusercontent.com/novalagung/mllpong/master/install.sh | sh

◉ Linux packages

Download the .deb, .rpm, or .apk from the latest release, then install:

# Debian / Ubuntu
sudo dpkg -i mllpong_linux_amd64.deb

# RHEL / Fedora
sudo rpm -i mllpong_linux_amd64.rpm

# Alpine
sudo apk add --allow-untrusted mllpong_linux_amd64.apk

◉ Docker

Available on both Docker Hub and GHCR:

# Docker Hub
docker pull novalagung/mllpong:latest

# GHCR
docker pull ghcr.io/novalagung/mllpong:latest

Running MLLPong

◉ Binary

Once installed, start the server with defaults (ACK on 2575, Chaos on 2576, Smart on 2577):

mllpong

With CLI flags:

mllpong --ack-port 2575 --chaos-port 2576 --smart-port 2577 --rules-file /etc/hl7/rules.json

To see all available options:

mllpong --help

◉ Docker

docker run -d \
  -e ACK_PORT=2575 \
  -e CHAOS_PORT=2576 \
  -e SMART_PORT=2577 \
  -p 2575:2575 \
  -p 2576:2576 \
  -p 2577:2577 \
  novalagung/mllpong:latest

Or with custom rules.json:

docker run -d \
  -e ACK_PORT=2575 \
  -e CHAOS_PORT=2576 \
  -e SMART_PORT=2577 \
  -e RULES_FILE=/etc/hl7/rules.json \
  -p 2575:2575 \
  -p 2576:2576 \
  -p 2577:2577 \
  -v ./rules.json:/etc/hl7/rules.json:ro \
  novalagung/mllpong:latest

◉ Docker Compose

services:
  mllpong:
    image: novalagung/mllpong:latest
    # build: .
    environment:
      HOST: "0.0.0.0"
      ACK_PORT: 2575
      CHAOS_PORT: 2576
      SMART_PORT: 2577
    ports:
      - "2575:2575"
      - "2576:2576"
      - "2577:2577"
    restart: unless-stopped

Or with custom rules.json:

services:
  mllpong:
    image: novalagung/mllpong:latest
    # build: .
    environment:
      HOST: "0.0.0.0"
      ACK_PORT: 2575
      CHAOS_PORT: 2576
      SMART_PORT: 2577
      RULES_FILE: /etc/hl7/rules.json
    ports:
      - "2575:2575"
      - "2576:2576"
      - "2577:2577"
    volumes:
      - ./rules.json:/etc/hl7/rules.json:ro
    restart: unless-stopped

Testing the Server

Send any valid HL7 v2 message wrapped in MLLP framing to any port. A quick smoke test with netcat:

# ACK handler — expect MSA|AA
printf '\x0bMSH|^~\&|sender|sender|receiver|receiver|20240101120000||ADT^A01^ADT_A01|MSG001|P|2.5\rEVN||20240101120000\rPID|||123456||Doe^John\r\x1c\x0d' | nc localhost 2575 | tr '\r' '\n'

# Chaos handler — expect MSA|AR
printf '\x0bMSH|^~\&|sender|sender|receiver|receiver|20240101120000||ADT^A01^ADT_A01|MSG002|P|2.5\rEVN||20240101120000\rPID|||123456||Doe^John\r\x1c\x0d' | nc localhost 2576 | tr '\r' '\n'

# Smart handler — response depends on rules.json
printf '\x0bMSH|^~\&|sender|sender|receiver|receiver|20240101120000||ADT^A01^ADT_A01|MSG003|P|2.5\rEVN||20240101120000\rPID|||123456||Doe^John\r\x1c\x0d' | nc localhost 2577 | tr '\r' '\n'

HL7 uses \r (carriage return) as the segment separator. Without | tr '\r' '\n' the response appears blank in the terminal because each segment overwrites the previous line.

Configuration

All options can be set via CLI flag or environment variable. CLI flags take precedence over env vars.

Flag Env var Default Description
--host HOST 0.0.0.0 Interface to bind
--ack-port ACK_PORT 2575 Port for the always-ACK handler
--chaos-port CHAOS_PORT 2576 Port for the always-NACK chaos handler
--smart-port SMART_PORT 2577 Port for the rule-based smart handler
--rules-file RULES_FILE rules.json Path to the smart handler rules JSON file

Smart Handler

The smart handler (port 2577) reads a JSON rules file at startup and matches each incoming message against the rules to decide the response. It supports per-message-type configuration, custom acknowledgment codes, artificial latency, and probabilistic chaos.

◉ Rule file format

{
  "rules": [
    {
      "match": "ADT^A01",
      "response": "AA",
      "ack_text": "Patient admitted"
    },
    {
      "match": "ORM",
      "response": "AE",
      "error_code": 207,
      "error_severity": "E",
      "error_msg": "Order processing failed"
    },
    {
      "match": "ADT^A08",
      "response": "AA",
      "nack_rate": 0.2,
      "ack_text": "Patient updated"
    },
    {
      "match": "SIU",
      "response": "AA",
      "delay_ms": 500
    },
    {
      "match": "*",
      "response": "AA",
      "ack_text": "Message accepted"
    }
  ]
}

See rules.json for complete example.

◉ Rule fields

Field Type Description
match string Message type to match. See matching rules below.
response string Acknowledgment code: AA (accept), AE (error), or AR (reject).
ack_text string Free text placed in MSA.3. Only meaningful for AA responses.
error_code int HL7 error code in the ERR segment. Defaults to 207.
error_severity string Severity in the ERR segment: E (error), W (warning), F (fatal). Defaults to E.
error_msg string Diagnostic message in the ERR segment.
delay_ms int Artificial response latency in milliseconds.
nack_rate float Probability (0.01.0) to override the configured response with AR. Useful for simulating intermittent failures.

◉ Match priority

Rules are evaluated in this order — the most-specific match wins:

  1. Exact"ADT^A01" matches only ADT messages with trigger event A01
  2. Type"ADT" matches any ADT message not covered by an exact rule
  3. Wildcard"*" matches any message not covered by type or exact rules

Match values are case-insensitive. If no rule matches at all, the server defaults to AA.

◉ Included rules.json

The repository ships with a rules.json that covers the most common HL7 v2 message types out of the box:

You can replace or extend this file without rebuilding the image.

Running Tests

The test suite is split into two layers:

Unit + coverage tests — no running server required, uses in-process TCP listeners:

go test ./...
go test ./... -cover   # with coverage report

Integration tests — connects to the real server on the configured ports:

go test -tags integration ./...

Override the default target addresses with environment variables:

Variable Default Description
TARGET_HOST localhost Host to connect to
ACK_PORT 2575 ACK handler port
CHAOS_PORT 2576 Chaos handler port
SMART_PORT 2577 Smart handler port

Local Build

Build then run the image:

docker build -t mllpong:local .
docker run -d \
  -e ACK_PORT=2575 \
  -e CHAOS_PORT=2576 \
  -e SMART_PORT=2577 \
  -e RULES_FILE=/etc/hl7/rules.json \
  -p 2575:2575 \
  -p 2576:2576 \
  -p 2577:2577 \
  -v ./rules.json:/etc/hl7/rules.json:ro \
  mllpong:local

Or use Docker Compose with a local build (already the default in the included docker-compose.yml):

docker compose up -d --build

License

MIT License

Maintainer

Noval Agung Prayogo