Skip to the content.

A lightweight mock server for HL7 v2 messages over MLLP (Minimal Lower Layer Protocol). Useful for testing HL7 integrations without a real receiver.

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

Pull the Image

Available on both Docker Hub and GHCR:

# Docker Hub
docker pull novalagung/mllpong:latest

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

Run with Docker

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

Omit SMART_PORT (and the related flags) to run without the smart handler.

Run with Docker Compose

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

To use local image, simply remove the image: novalagung/mllpong:latest replace it with build: ..

Environment Variables

Variable Default Description
HOST 0.0.0.0 Interface to bind
ACK_PORT 2575 Port for the always-ACK handler
CHAOS_PORT 2576 Port for the always-NACK chaos handler
SMART_PORT (disabled) Port for the rule-based smart handler; omit to disable
RULES_FILE rules.json Path to the smart handler rules config 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"
    }
  ]
}

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.

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.

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