Tolerate a sender's framing quirks
Real senders do not always write the canonical block VT payload FS CR. Some end a frame with
FS alone, some put a line feed after it, some pad before the VT, and a few omit the VT
entirely. This guide shows how to accept each of those deviations deliberately, and how to see
every one you accepted. Framing and tolerance explains the model behind it.
The reference entries used here are FrameReader,
FrameReaderOptions, MllpWarning
and MllpFramingError. Every code is listed in
Warning and error codes.
The four flags
| Flag | Accepts | Warning code | FrameReader default | Server default |
|---|---|---|---|---|
allowFsOnly | a frame that ends FS with no CR | MLLP_FS_WITHOUT_CR | off | on |
allowLfAfterFs | a line feed after FS | MLLP_LF_AFTER_FS | off | on |
allowLeadingWhitespace | padding before VT | MLLP_LEADING_WHITESPACE | off | on |
allowMissingLeadingVt | a frame with no leading VT | MLLP_MISSING_LEADING_VT | off | off |
With a flag off, the deviation is not tolerated: FrameReader.push() throws MllpFramingError
with that code, and a connection that meets it is closed. Leading whitespace is the exception: it is
refused with the code MLLP_MISSING_LEADING_VT, so MLLP_LEADING_WHITESPACE only ever arrives as a
warning. With the flag on, the payload is
recovered and the code is reported as a warning. strict: true turns all four off again, whatever
the individual flags say, and strict leading whitespace is refused with MLLP_MISSING_LEADING_VT
as well.
On a standalone reader
A FrameReader you construct yourself is strict until you opt in:
import { FrameReader, MllpFramingError } from "@cosyte/mllp";
// <VT> "A" <FS> <LF>: a line feed where the <CR> belongs.
const bytes = Buffer.from([0x0b, 0x41, 0x1c, 0x0a]);
let refused = "";
try {
new FrameReader({ onFrame: () => {} }).push(bytes);
} catch (err) {
refused = err instanceof MllpFramingError ? err.code : "other";
}
refused; // => "MLLP_LF_AFTER_FS"
const codes: string[] = [];
const frames: string[] = [];
new FrameReader({
onFrame: (payload) => frames.push(payload.toString("latin1")),
onWarning: (warning) => codes.push(warning.code),
allowLfAfterFs: true,
}).push(bytes);
frames; // => ["A"]
codes; // => ["MLLP_LF_AFTER_FS"]
Leading whitespace is refused under a different code from the one it warns with:
import { FrameReader, MllpFramingError } from "@cosyte/mllp";
// A space, then <VT> "A" <FS> <CR>.
const padded = Buffer.from([0x20, 0x0b, 0x41, 0x1c, 0x0d]);
const refusedCode = (options: { allowLeadingWhitespace?: boolean; strict?: boolean }): string => {
try {
new FrameReader({ onFrame: () => {}, ...options }).push(padded);
return "accepted";
} catch (err) {
return err instanceof MllpFramingError ? err.code : "other";
}
};
refusedCode({}); // => "MLLP_MISSING_LEADING_VT"
refusedCode({ allowLeadingWhitespace: true, strict: true }); // => "MLLP_MISSING_LEADING_VT"
const warned: string[] = [];
new FrameReader({
onFrame: () => {},
onWarning: (warning) => warned.push(warning.code),
allowLeadingWhitespace: true,
}).push(padded);
warned; // => ["MLLP_LEADING_WHITESPACE"]
On a server
A server applies allowFsOnly, allowLfAfterFs and allowLeadingWhitespace to every connection it
accepts, because it is the side that has to take what senders write. Its onWarning option sees
every warning from every connection:
import { connect } from "node:net";
import { createServer } from "@cosyte/mllp";
const codes: string[] = [];
const server = createServer({
autoAck: "AA",
onMessage: async () => {},
onWarning: (warning) => codes.push(warning.code),
});
await server.listen(0);
// A sender that ends its frame <FS> <LF> instead of <FS> <CR>.
const socket = connect(server.getStats().port ?? 0, "127.0.0.1");
await new Promise((resolve) => socket.once("connect", resolve));
const reply = new Promise<Buffer>((resolve) => socket.once("data", resolve));
socket.write(
Buffer.concat([
Buffer.from([0x0b]),
Buffer.from("MSH|^~\\&|APP|FAC|RECV|RFAC|20260101120000||ADT^A01|CTRL0301|P|2.5.1\r"),
Buffer.from([0x1c, 0x0a]),
]),
);
(await reply).toString("latin1").includes("MSA|AA|CTRL0301"); // => true
codes; // => ["MLLP_LF_AFTER_FS"]
socket.destroy();
await server.close();
Pass framing to change those defaults. Whatever you pass is merged over them, so
framing: { strict: true } makes a server as strict as a bare reader, and
framing: { allowMissingLeadingVt: true } adds the fourth flag. Turn allowMissingLeadingVt on
only for a sender you have identified: a stream with no VT gives the reader no reliable way to
tell where a message starts.
The client takes the same framing option for the acknowledgments it reads, and applies none of
the flags unless you pass them.
Bound the frame size
A reader buffers until it sees FS, so a peer that opens a frame and never closes it would grow
the buffer without end. maxFrameSizeBytes caps it; the default is 16 MiB (16,777,216 bytes).
Past the cap, push() throws MllpFramingError with MLLP_FRAME_TOO_LARGE, and on a server or
client that closes the connection:
import { FrameReader, MllpFramingError } from "@cosyte/mllp";
const reader = new FrameReader({ onFrame: () => {}, maxFrameSizeBytes: 8 });
let code = "";
try {
reader.push(Buffer.from([0x0b, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
} catch (err) {
code = err instanceof MllpFramingError ? err.code : "other";
}
code; // => "MLLP_FRAME_TOO_LARGE"
On a server, set it through framing: { maxFrameSizeBytes }.
Log warnings without the payload
A warning carries a code, the byteOffset in the stream where the deviation was found, the
connectionId (on a server or client) and a timestamp. Log those. The message of two codes,
MLLP_MISSING_LEADING_VT and MLLP_FS_WITHOUT_CR, includes the one byte found where a framing byte
was expected, which on an unframed stream is the first byte of the message; if your logs may hold
no message content at all, leave message out.