HL7: BizTalk HL7 Accelerator parse errors on ACK / NACK
Next one: You are getting parse errors on the (Negative) Acknowledgment BizTalk wants to send back to the sending system.
The scenario
BizTalk receives an HL7 message with an error in the header, e.g. MSH|&^\|APP1^^^^^^^^^||APP2||.
The MSH_3 data-type will trigger an error, for it’s not valid. The message won’t be parsed and a negative acknowledgement will be generated. The NACK is being picked up by the receive port to return it to the sending system. But hey, a parse error happens in the HL7 pipeline?! Thats weird…
Well, what happened here is that the HL7 accelerator swapped MSH-3 with MSH-5 and I guess you are thinking… NOOOO. But YES! Now on the way back, parsing the generated NACK XML to an HL7 flatfile, the same parse error occurs, but on a different field!
The problem occurs when we are trying to parse the acknowledgement being sent back to the sending system. So basically the error generated is coming from the HL7 accelerator. After the error occurs the message will be suspended and we will never be able to get the message parsed correctly.
The solution
This is a good one. I had to make a wrapper pipeline component, which wraps the standard HL7XAsm pipeline component (PC) and implemented some error handling to be sure an acknowledgement will always be sent out.
If an error occurs in the normal HL7XAsm PC, then I will try and concatinate a message myself and try and Assemble it again with the HL7XAsm PC. If this still errors, a negative acknowledgement will be sent out, a generated flat file. So then we won’t try to parse the message, but we ignore the standard HL7XAsm and just make sure the sending system at least gets a negative acknowledgement containing a message saying an unhandled error occured inside BizTalk.
It’s important to always return a message to the sending system, otherwise it may retry the errored message endlessly.
I will try and explain how I tried to solve this. I used some undocumented or unsupported features of BizTalk, but hey, try and live a little bit on the edge!
First clone your context. Don’t copy, but clone!
1 |
IBaseMessageContext origContext = PipelineUtil.CloneMessageContext(inmsg.Context); |
After this you can try and get the data of the three message parts. If these are empty, you already have a problem, so make sure this is within a try-catch block.
1 2 3 |
Stream origMSHSegment = inmsg.GetPart( "MSHSegment" ).Data; Stream origBodySegments = inmsg.GetPart( "BodySegments" ).Data; Stream origZSegments = inmsg.GetPart( "ZSegments" ).Data; |
Then we’ll have to call the original pipeline.
1 2 3 |
Microsoft.Solutions.BTAHL7.Pipelines.HL72fAsm HL72fAsm = new Microsoft.Solutions.BTAHL7.Pipelines.HL72fAsm(); HL72fAsm.TrailingDelimiterAllowed = this .TrailingDelimitersAllowed; |
And now try to assemble the message. If this next line of code executes correctly, nothing is wrong and you can just send out the message.
1 2 3 4 5 |
try { HL72fAsm.AddDocument(pc, inmsg); inmsg = HL72fAsm.Assemble(pc); } |
If this goes wrong, we will try and parse it for a second time. The parse error is most likely to come from the header and we basically build up a new header and leave the rest of the message the same. We try and gather as much information as we can from the original header, but not too much, for then we may also copy the data that was causing the error in the first place.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// First copy the context from the original context. // The context has changes due to our first attempt. inmsg.Context = origContext; inmsg.GetPart( "MSHSegment" ).Data = origMSHSegment; inmsg.GetPart( "BodySegments" ).Data = origBodySegments; inmsg.GetPart( "ZSegments" ).Data = origZSegments; StreamReader origHeader = new StreamReader(inmsg.GetPart( "MSHSegment" ).Data); string origHeaderString = origHeader.ReadToEnd(); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(origHeaderString); // Now extract all the info that is usefull from the header and build a // new header. So use Xpaths to copy MSH3.1, MSH5.1 (notice, it's only // the first field), the message controlId, the versionId and use these values // to construct a new header string MSH31 = xmlDoc.DocumentElement.SelectSingleNode( "//MSH.3_SendingApplication/HD.0_NamespaceId" ).InnerText; string MSH51 = xmlDoc.DocumentElement.SelectSingleNode( "//MSH.5_ReceivingApplication/HD.0_NamespaceId" ).InnerText; // etcetera. // Now replace the header in the inmsg and try to parse the // whole thing again, something like this byte [] outputBufferHeader = Encoding.Default.GetBytes(xmlDocNewHeader.OuterXml); MemoryStream outputStream = new MemoryStream(); outputStream.Write(outputBufferHeader, 0, outputBufferHeader.Length); outputStream.Seek(0, SeekOrigin.Begin); inmsg.GetPart( "MSHSegment" ).Data = outputStream; HL72fAsm.AddDocument(pc, inmsg); inmsg = HL72fAsm.Assemble(pc); |
If this goes allright, then we are good and return this inmsg. If not, ok, then we have problem. Since we are not clairvoyant, or at least most of us aren’t, we can go on and on with trying to get the message parsed. Well, 2 times is enough for me and after this one fails, I decided to build a custom NACK, stating BizTalk encountered a fatal error. I still try and get as much info as possible in this custom NACK, but I am not parsing it through the standard HL7XAsm PC anymore. I build a custom flatfile, just as an HL7 Acknowledgement looks like and use some logic for that and output this flatfile.
And your flat output may look like this. You can of course try and get some more variables. Many ifs, tries and catches here
1 2 |
string errHdr = @"MSH|^~^\&|SNDAPP||RCVAPP||[DateTime]||[MsgType]^[Trigger]|BTSERR001|P|[Version]|"; string errBody = @"MSA|AE|UNKOWN|A fatal error has occured in BizTalk. please contact the BizTalk administrator to solve this problem. the original error message was: " + ex.Message; |
A long story. In a nuttshell: if you’re incoming header causes parse errors, your outgoing header may also cause parse errors. We have to make sure a message is returned to the sending system, so a custom ACK is generated if something goes wrong.
Hopefully this helps. If not, feel free to contact me if you have any questions on this issue.
Dear Robbie, can we get access to the full source of this plugin by any chance?