Help

Troubleshooting

Symptom, cause, and fix for the failures people actually hit. Most of them are one of four things: a repetition, the HL7 null, a release mismatch, or a segment that does not exist yet.

Start here

Three commands answer most questions faster than reading code, and none of them needs a project.

sh
# Does it parse at all, and what is actually in it?
hl7-v2 --paths message.hl7

# What does the dictionary think is wrong with it?
hl7-v2 --check message.hl7; echo "exit $?"

# Does the path you are using name anything?
hl7-v2 --query 'PID-3.1' message.hl7

Exit status from --check is meaningful: 0 is fine, 1 means it is not a message at all, and 2 means it is a message and something is wrong with it.

Parsing fails

Error::Empty — “input contains no HL7 segments”
The input was empty or whitespace. If you read from a socket, you may have read zero bytes; if from a file, check you are not passing a directory or an empty file.
Error::MissingMsh — “message does not start with an MSH segment”
Something is in front of the header. Common causes: a byte-order mark, a leading blank line, an MLLP start block that was not stripped, or an HTTP body still carrying its headers. Also check you are not handing a whole batch file to parse — use split_messages first.
Error::BadMshHeader — “malformed MSH header”
MSH-1 and MSH-2 declare the delimiters, and this message's are unusable. Look at the raw bytes: a file that has been through a text editor may have had its carriage returns converted, which can merge the header with the next segment.
sh
# Segment terminators are carriage returns. If a file has been through a
# text editor or a Windows share, check what is actually in it.
od -c message.hl7 | head

Reading gives the wrong thing

get returns None for a path you can see in the message
Check the index. Paths are one-based, and a segment that occurs more than once needs SEG[n]. Run hl7-v2 --paths and copy the bracketed path exactly.
get returns an empty string and you cannot tell what that means
An explicit HL7 null and an empty field both read as an empty string from a path.
rust
// Both come back as Some("") — the explicit null and an empty field.
message.get("PID-2")?;

// Read the node when you need to tell them apart.
message.tree().find("PID.2");
The wrong identifier comes back
PID-3 repeats. An unindexed path reads the first repetition, and the order is not guaranteed.
rust
// Wrong: reads whichever identifier the sender happened to put first.
let id = message.get("PID-3.1")?;

// Right: select by assigning authority.
for index in 1..=message.repetitions("PID-3")?.len() {
    if message.get(&format!("PID-3[{index}].4"))?.as_deref() == Some("NHS") {
        let id = message.get(&format!("PID-3[{index}].1"))?;
    }
}
A component reads as a whole field, or splits in the wrong place
The dictionary does not know that field's data type — either because the release resolved differently from what you expected, or because the field is past the end of the published segment. Check with message.type_of("PID-3"), and see Vendor dictionaries to name it.
find returns nothing for a name you can see in the tree
Names come from the dictionary, so a field it does not recognise has a positional name (PID.5.1) rather than a typed one (XPN.1). Search for the positional form, or supply a dictionary.

Writing fails or writes the wrong thing

Error::NoSuchSegment
The write named a segment the message does not have. Add it first — and remember the index afterwards.
rust
// Error::NoSuchSegment — the write named a segment the message does not have.
message.append_segment("NTE");
message.set("NTE[2]-3", "Amended.")?;
Error::UnwritablePath
The path named something that cannot be written: a whole segment, or a repeating value without a field. Address the specific field.
The far end sees a literal \F\ in the text
The value was escaped twice.
rust
// Wrong: the value is escaped twice, and the far end sees a literal \F\
message.set("OBX-5", "90\\F\\100")?;

// Right: pass the value you mean. The library owns the encoding.
message.set("OBX-5", "90|100")?;
A field you meant to clear came out as a deletion instruction, or the other way round
set_null writes the explicit HL7 null, meaning “delete the value you hold”. clear removes the value entirely, meaning “nothing is being said about this”. In an update message they do opposite things.

Validation says something unexpected

Warnings about segments and fields you know are fine
A warning means the dictionary does not cover the message, not that the message is wrong. It is the signal to write a dictionary — not a reason to reject.
Strict mode rejects a message the sender insists is valid
Look at which diagnostics are error-level. The four error kinds are a missing MSH-9.1 or MSH-10, a required segment or group absent, segments that do not fit the structure, and an SI/NM/DT/TM/DTM value that is not one. A structure mismatch caused only by Z-segments is a warning, not an error.
Nothing is reported about a field you know is wrong
Only value formats with a machine-checkable shape are checked. ST, TX, ID, and IS are constrained by HL7 tables and lengths, which are not modelled — the crate says nothing rather than guessing. Table membership and clinical plausibility are your domain layer's job.

Conversion output is not what you expected

Everything came out flat instead of grouped
The message's segments did not fit its declared structure — usually a Z-segment, or a structure with no built-in grammar. Grammars exist for ACK, ADT_A01 (and A04/A08/A13), ORM_O01, and ORU_R01. Flat output is well-formed and lossless.
Elements are named PID.5.1 instead of XPN.1
The dictionary did not recognise the type at that position. Supply one with --dictionary.
A JSON key is sometimes a string and sometimes an array
By design: a repeatable key is a bare value when it occurs once and an array only when it occurs more than once. Consumers that need a uniform shape must normalize.
A segment vanished from the JSON output
A segment name that repeats non-adjacently is grouped at its first occurrence, because a JSON object cannot carry the same key twice. Use the XML mapping if document order matters.
The round trip does not match the input file
It is not meant to — the output is the original message canonicalized. Compare against hl7-v2 --er7 input.hl7. See A lossless round trip.
The XML does not validate against the vendor's XSDs
Pass both --dictionary and --schema-shape. Without the second, the dictionary only names what a field is; with it, the dictionary decides what the document contains.

Transport problems

receive returns an InvalidData error
Framing was violated — a frame did not start with <VT>, did not end with <FS><CR>, or contained a block character in between. A stream that has lost framing cannot be resynchronized: close the connection. If the sender's only sin is a missing trailing <CR>, set Tolerance::Lenient for that connection.
The connection ends after one message
Keep looping on receive. MLLP connections are long-lived and carry many messages; closing after one is a common bug and senders notice immediately.
The sender keeps retrying even though you answered
Check that MSA-2 echoes the incoming MSH-10. That echo is the only thing that says which message you answered.
A frame is never delivered and memory grows
A peer that never sends an end block. The Framer caps its buffer at 16 MiB by default and errors past it; lower the limit to your largest real message plus headroom, and add a read timeout.
A SOAP endpoint rejects your envelope, or you reject theirs
Prefixes are matched on local name, so that is not it. Check for more than one child in the body — one payload per body is enforced deliberately.
A SOAP sender retries forever against a working endpoint
The two acceptance conventions. response::evaluate accepts AA, CA, and Success in a v2 Status element; an endpoint writing something else will read as a rejection.

Build and dependency problems

cannot find derive macro FromHl7
The derive feature is off. Add features = ["derive"] to hl7-2 or to hl7.
cannot find derive macro FromElement
Same, but on hl7-3 specifically — the umbrella crate's derive feature forwards to hl7-2's only.
acknowledge_now does not exist
It is behind hl7-2-mllp's clock feature, which is off by default.
ack module does not exist
You built with --no-default-features, which turns off the ack feature to give you framing with zero dependencies.
The crate does not build on your toolchain
Check your Rust version against the crate's rust-version. The floor is current stable minus three releases. If you are at or above it and it still fails, that is a bug worth reporting.
cargo install hl7-2 succeeded but hl7-2 is not a command
The binary is named hl7-v2.

Still stuck

Read the crate's spec/index.md — it is numbered section by section, and the section that covers your symptom usually says exactly what the crate does and why. If the spec says one thing and the crate does another, that is a bug and worth reporting with the section number. See Support and contributing.