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.
# 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.hl7Exit 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— usesplit_messagesfirst. Error::BadMshHeader— “malformed MSH header”MSH-1andMSH-2declare 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.
# 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 | headReading gives the wrong thing
getreturnsNonefor 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]. Runhl7-v2 --pathsand copy the bracketed path exactly. getreturns 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.
// 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-3repeats. An unindexed path reads the first repetition, and the order is not guaranteed.// 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. findreturns 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.
// 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.
// 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_nullwrites the explicit HL7 null, meaning “delete the value you hold”.clearremoves 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.1orMSH-10, a required segment or group absent, segments that do not fit the structure, and anSI/NM/DT/TM/DTMvalue 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, andISare 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.1instead ofXPN.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
--dictionaryand--schema-shape. Without the second, the dictionary only names what a field is; with it, the dictionary decides what the document contains.
Transport problems
receivereturns anInvalidDataerror- 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>, setTolerance::Lenientfor 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-2echoes the incomingMSH-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
Framercaps 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::evaluateacceptsAA,CA, andSuccessin a v2Statuselement; an endpoint writing something else will read as a rejection.
Build and dependency problems
cannot find derive macro FromHl7- The
derivefeature is off. Addfeatures = ["derive"]tohl7-2or tohl7. cannot find derive macro FromElement- Same, but on
hl7-3specifically — the umbrella crate'sderivefeature forwards tohl7-2's only. acknowledge_nowdoes not exist- It is behind
hl7-2-mllp'sclockfeature, which is off by default. ackmodule does not exist- You built with
--no-default-features, which turns off theackfeature 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-2succeeded buthl7-2is 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.