Introducción: las tramas son la capa que todos olvidan
Cuando una interfaz HL7 v2 falla, los equipos de integración acuden instintivamente al propio mensaje: inspeccionan MSH-9, cuentan campos, revisan delimitadores y buscan segmentos malformados. Pero una buena parte de las incidencias de tipo "la interfaz dejó de funcionar" no tienen nada que ver con el contenido del mensaje. Son fallos de tramado: los bytes que envuelven cada mensaje HL7 mientras viaja por una conexión TCP están corruptos, ausentes o se interpretan mal. El receptor o bien nunca reconoce el inicio de un mensaje, o nunca ve el final de uno, o concatena dos mensajes en un único bloque imposible de analizar.
El protocolo de tramado utilizado por prácticamente toda interfaz HL7 v2 en producción es el Protocolo de Capa Inferior Mínima (MLLP), definido en la HL7 Version 2 Transport Specification — MLLP, Release 2 (un estándar aprobado por ANSI/HL7). MLLP es engañosamente simple: envuelve cada mensaje en tres bytes de control. Esa simplicidad es precisamente la razón de que los errores de tramado sean tan comunes: el protocolo asume que los bytes llegan intactos y no ofrece ninguna suma de verificación, prefijo de longitud ni mecanismo de recuperación integrados. Esta guía explica cómo funciona el tramado MLLP a nivel de byte, recorre los patrones de error más comunes incluido el fallo canónico "0x0B / 0x1C ausente" y ofrece un enfoque de diagnóstico concreto para cada uno.
Nota: Este artículo es de referencia educativa y técnica. Los pasos de configuración de motores de interfaz varían según la versión y el proveedor. Sigue siempre los procedimientos de gestión del cambio de tu organización antes de modificar configuraciones de interfaces de producción.
Cómo funciona el tramado MLLP a nivel de byte
MLLP define un único sobre de mensaje construido a partir de tres caracteres de control que nunca aparecen en texto HL7 v2 bien formado:
- Bloque de inicio (SB): un único byte,
0x0B(tabulación vertical, VT). Marca el comienzo de un mensaje. - Bloque de fin (EB): un único byte,
0x1C(separador de archivo, FS). Marca el final del contenido del mensaje. - Retorno de carro (CR): un único byte,
0x0D. Sigue inmediatamente al bloque de fin y termina el sobre.
Así, una trama MLLP completa en el cable es: 0x0B + <mensaje HL7> + 0x1C + 0x0D. La HL7 Version 2 Transport Specification (MLLP, Release 2) es explícita en que el receptor debe descartar cualquier byte que llegue antes de un bloque de inicio, y debe tratar todo lo que hay entre el bloque de inicio y el bloque de fin como contenido del mensaje. No hay campo de longitud. El receptor es una máquina de estados a nivel de byte: espera 0x0B, acumula bytes hasta que ve 0x1C 0x0D y luego entrega el búfer acumulado al analizador HL7.
Un detalle sutil pero importante: dentro de la carga HL7, los segmentos se separan con un retorno de carro (0x0D) según el estándar base HL7 v2.x (Capítulo 2, "Control"). Es el mismo byte usado en el finalizador MLLP. Esa coincidencia es la raíz de toda una familia de errores de tramado, porque una implementación ingenua que busca un 0x0D suelto para detectar el final de un mensaje terminará en el primer separador de segmento en lugar de en el finalizador.
Modo de fallo 1: bloque de inicio ausente (0x0B)
Este es el fallo de tramado de manual. La máquina de estados MLLP del receptor está en su estado de "esperando bloque de inicio", llegan bytes, pero ninguno es 0x0B. Según la especificación, el receptor descarta silenciosamente cada byte hasta encontrar un bloque de inicio, por lo que el mensaje entero se consume y se desecha, y nunca se genera ningún ACK.
Identificando el patrón
La señal distintiva es la asimetría: los registros del emisor muestran que un mensaje se transmitió correctamente, la conexión TCP está sana, pero el registro de aplicación del receptor no muestra nada, ni siquiera un error de análisis. El mensaje simplemente desapareció. En una captura de paquetes (Wireshark, tcpdump), verás que los bytes de la carga comienzan directamente con MSH (0x4D 0x53 0x48) sin un 0x0B inicial. El receptor nunca registra nada porque, desde su perspectiva, ningún mensaje llegó a empezar.
Causas habituales
- El emisor omite el tramado MLLP por completo. Un cliente TCP artesanal escribe la cadena HL7 cruda en el socket sin envolverla. Es extremadamente común cuando un script escrito para pruebas se promociona a producción.
- Semántica de puerto incorrecta. El emisor está configurado para un punto final HTTP, SFTP o TCP plano de depósito de archivos, pero el receptor espera MLLP. Los bytes llegan, pero el contrato de tramado no coincide.
- Búfer residual tras un error de finalizador previo. Si un mensaje anterior tuvo un finalizador malformado, el analizador del receptor puede haberse desincronizado, dejando bytes residuales del mensaje anterior que enmascaran el siguiente bloque de inicio.
Pasos de diagnóstico
Captura los bytes crudos en el host receptor a nivel de red, no a nivel de aplicación: la capa de aplicación ya ha eliminado o rechazado el tramado cuando ves los registros. Inspecciona el primer byte de la carga TCP. Si no es 0x0B, la culpa es del emisor. Confírmalo extrayendo la carga y pegándola en nuestro visor de mensajes HL7: si el visor lo analiza limpiamente como un mensaje válido, has demostrado que el contenido está bien y has aislado el problema al tramado. Para una segunda comprobación estructurada del cuerpo del mensaje, pásalo por nuestro validador de mensajes HL7 y descarta cualquier problema de contenido disfrazado de problema de transporte.
Modo de fallo 2: bloque de fin ausente o truncado (0x1C 0x0D)
Aquí el bloque de inicio llega correctamente, el receptor empieza a acumular bytes, pero la secuencia de cierre 0x1C 0x0D nunca aparece, o solo aparece 0x1C sin el 0x0D final. La máquina de estados del receptor permanece indefinidamente en el estado de "leyendo mensaje", acumulando bytes y esperando un finalizador que no llegará.
Identificando el patrón
La firma es un mensaje colgado o perpetuamente "en progreso" en el receptor mientras el emisor cree que el mensaje se entregó. Eventualmente salta el timeout de lectura del receptor (habitualmente 30–60 segundos) y la conexión se derriba, a menudo registrado como un timeout de socket genérico en lugar de un error de tramado, lo que lanza a los ingenieros a perseguir un problema de red que no existe. Si el emisor reintenta, puede desarrollarse un bucle de retransmisión similar a un timeout de ACK, pero la causa subyacente es estructural, no de red.
Causas habituales
- El emisor escribe solo el bloque de fin, no el CR. Algunas implementaciones olvidan que el finalizador son dos bytes (
0x1C 0x0D), no uno. Un receptor estricto espera para siempre el0x0D. - La carga HL7 contiene un
0x1Cincrustado. Aunque0x1Cnunca debería aparecer en texto v2 bien formado, datos binarios colocados indebidamente en un campo OBX-5 (sin encapsulación adecuada) pueden introducir uno, provocando un bloque de fin prematuro. - Segmentación TCP entre paquetes. El finalizador se divide entre dos segmentos TCP y un receptor frágil gestiona mal el límite, ver el modo de fallo 3.

Modo de fallo 3: fragmentación y concatenación de mensajes
Este es el fallo de tramado más sutil, y deriva directamente de que MLLP no tiene prefijo de longitud. TCP es un flujo de bytes, no un flujo de mensajes: la red puede entregar una única trama MLLP en varias llamadas recv(), o entregar dos tramas completas en una sola llamada recv(). Según la especificación HL7 MLLP Release 2, un receptor conforme DEBE tratar el flujo como una secuencia continua de bytes y basarse únicamente en los caracteres de control 0x0B y 0x1C 0x0D para delimitar mensajes. Las implementaciones que asumen que "una lectura equivale a un mensaje" se rompen de dos formas opuestas.
Sub-tramado: un mensaje dividido entre lecturas
Una interfaz de alto rendimiento envía un resultado ORU^R01 grande con texto de informe incrustado. El sistema operativo lo entrega en tres segmentos TCP. Un receptor ingenuo que analiza cada búfer de recv() de forma independiente ve un bloque de inicio en la primera lectura pero ningún bloque de fin, y o descarta el búfer parcial o falla. La solución es mantener un búfer de acumulación entre lecturas hasta encontrar el finalizador, que es exactamente lo que exige la especificación.
Sobre-tramado: dos mensajes en una lectura
Bajo carga en ráfaga, dos tramas completas — 0x0B...0x1C 0x0D 0x0B...0x1C 0x0D — llegan en una sola lectura. Un receptor que no busca todos los límites de tramado en el búfer entregará ambos mensajes, caracteres de control incluidos, al analizador HL7 como una sola unidad. El analizador ve un segundo MSH a mitad del flujo y rechaza todo, o peor, destroza silenciosamente el segundo mensaje. En una captura de paquetes esto es inconfundible: busca dos bytes 0x0B dentro de una única carga TCP.
Pasos de diagnóstico
Reensambla el flujo TCP completo en Wireshark (Follow TCP Stream) en lugar de examinar paquetes individuales. Representa la carga en hexadecimal y cuenta los caracteres de control: todo mensaje bien formado debe tener exactamente un 0x0B inicial y exactamente un 0x1C 0x0D final. Si ves 0x0B sin un finalizador correspondiente en la misma unidad lógica, tienes sub-tramado; si ves dos bytes 0x0B antes del primer 0x1C 0x0D, tienes sobre-tramado. El remedio en ambos casos es un lector MLLP correctamente bufferizado y guiado por caracteres de control.
Modo de fallo 4: corrupción de codificación y juego de caracteres de los bytes de control
Los bytes de tramado son valores crudos de 8 bits, pero atraviesan pilas de software que pueden intentar "ayudar" transcodificando texto. El estándar HL7 v2.x declara el juego de caracteres del mensaje en MSH-18, y el valor base por defecto es una codificación de un solo byte. Los problemas surgen cuando una capa entre emisor y receptor asume UTF-8 o UTF-16 y recodifica el flujo de bytes.
- Envoltura UTF-16. Si un emisor escribe la trama como UTF-16, cada byte se intercala con un
0x00. El0x0Bse convierte en0x0B 0x00(o0x00 0x0B), y un receptor MLLP exacto a nivel de byte ya no reconoce el bloque de inicio. Esto ocurre con frecuencia cuando un componente .NET o Java usa unWriter/Encodingpor defecto en lugar de escribir bytes crudos. - Traducción de saltos de línea. Una biblioteca o sistema operativo que "normaliza" los finales de línea puede convertir el separador de segmento
0x0Den0x0D 0x0Ao en un0x0Asuelto. Según HL7 v2.x Capítulo 2, el terminador de segmento es estrictamente0x0D; un receptor endurecido a esa regla rechazará el flujo alterado, y el0x0Dfinal del finalizador MLLP también puede corromperse. - Proxies que recortan espacios en blanco. Los balanceadores de carga de capa de aplicación o el middleware de conversión de protocolo a veces eliminan caracteres no imprimibles iniciales/finales, suprimiendo silenciosamente el
0x0Bo el0x1C.
El diagnóstico es siempre el mismo: obtén un volcado hexadecimal exacto a nivel de byte en ambos extremos y compara. Si hay bytes 0x00 intercalados, tienes un problema de UTF-16; si han aparecido bytes 0x0A junto a tus bytes 0x0D, tienes traducción de saltos de línea. La solución es asegurar que cada capa trate la trama MLLP como binario opaco, nunca como texto que recodificar.
Una lista de verificación de diagnóstico repetible
Cuando una interfaz v2 falla y sospechas del tramado, aborda el problema en este orden:
- Captura a nivel de red. Usa Wireshark o
tcpdumpen el host receptor y sigue el flujo TCP. Nunca confíes solo en los registros de aplicación: están por encima de la capa de tramado. - Volcado hexadecimal de la carga. Confirma exactamente un
0x0Bal principio y exactamente un0x1C 0x0Dal final. Cualquier otra cosa es un fallo de tramado. - Aísla el contenido del transporte. Quita los bytes de tramado y carga el mensaje desnudo en nuestro visor HL7. Si se analiza limpiamente, el contenido es inocente y el error está en el tramado.
- Comprueba ambas direcciones. Recuerda que el ACK viaja de vuelta dentro de su propia trama MLLP. Un error de tramado en la ruta de respuesta produce síntomas que imitan un timeout de ACK; para ese escenario, consulta nuestra guía complementaria sobre depuración de fallos ACK en HL7.
- Descarta el disfraz de contenido. Pasa el mensaje por nuestro validador HL7 para confirmar que ningún segmento malformado se está reportando erróneamente como un error de transporte, y consulta nuestra guía más amplia para solucionar problemas de interfaces HL7 para los modos de fallo a nivel de contenido.
Por qué los errores de tramado persisten en producción
Los errores de tramado MLLP son duraderos precisamente porque son invisibles para la mayoría de la monitorización. Un mensaje que falla en el tramado normalmente no produce ningún error a nivel de aplicación, ningún fallo de validación y ningún ACK, solo silencio o un timeout de socket genérico. El estándar no ofrece suma de verificación ni número de secuencia para detectar corrupción, por lo que la única evidencia autoritativa vive en el flujo de bytes crudo. Los equipos que construyen observabilidad consciente del tramado — registrando el primer y último byte de cada trama recibida, alertando ante bloques de inicio o fin ausentes, y capturando muestras periódicas a nivel de red — detectan estos fallos en minutos en lugar de días. La HL7 V2 Transport Specification (MLLP, Release 2) es un documento corto; leerlo de principio a fin es una de las horas de mayor rentabilidad que un ingeniero de integración puede invertir, porque cada detalle de implementación que especifica se corresponde directamente con un modo de fallo de producción que, de otro modo, se diagnosticaría por conjeturas.
La lección recurrente es que el sobre MLLP debe tratarse como binario opaco desde el momento en que abandona el socket del emisor hasta el momento en que la máquina de estados del receptor lo consume. Cada transformación intermedia — conversión de juego de caracteres, normalización de saltos de línea, división de búfer, recorte de espacios en blanco — es un fallo de tramado potencial. Cuando interiorizas que los tres bytes 0x0B, 0x1C y 0x0D son sagrados e inmutables, la resolución de problemas de MLLP deja de ser un misterio y se convierte en un ejercicio mecánico de conteo de bytes.