Connecting to Quest Lab 360 Using SOAP

Introduction

This article was originally written for iguana 5 and may contain out of date references.

We’re not big fans of, but there are times you have to deal with it. One of those instances if you want to connect your EMR electronically to Quest Diagnostics care360 so that physicians can request and receive lab orders and results without paper.

Like many web service APIs in healthcare Quest’s web service API consists of a SOAP wrapper around an HL7 message. These types of interfaces are interesting because the plumbing always tends to show through. Looking at the error messages coming out of this interface it appears to be an older system which speaks HL7, which is then wrapped in a SOAP style web service API implemented using BEA Weblogic.

In this case SOAP is just a transport envelope you have to get through before you can get at the actual data, which is the HL7 message encoded in base64. This must be done before you can get to the meat of the problem which is simply processing HL7 messages.

We also used a similar approach with the Surescript E-Prescription interface.

How the plumbing works [top]

The Quest 360 interface uses HTTP basic authentication over HTTPS which is quite easy to implement. The error messages the web service gave good hints that this would be the case. It’s a very common way to implement authentication with web services. If you get the return HTTP code of 401 it indicates the user name and password were wrong.

Then it was a matter of taking example SOAP envelope from the Quest diagnostics documentation and slotting in the HL7 message payload encoding it in base64.

That’s what this code does:

T = [==[<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ord="http://medplus.com/orders">
   <soapenv:Header/>
   <soapenv:Body>
      <ord:submitOrder soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
         <order xsi:type="java:Order" xs:type="type:Order" xmlns:java="java:com.medplus.serviceHub.orders.webservice" xmlns:xs="http://www.w3.org/2000/XMLSchema-instance">
            <hl7Order xsi:type="xsd:base64Binary">${globalCleanHL7}</hl7Order>
         </order>
      </ord:submitOrder>
   </soapenv:Body>
</soapenv:Envelope>]==]

function MakePayload(HL7)
   local P = xml.parse{data=T}
   P["soapenv:Envelope"]["soapenv:Body"]["ord:submitOrder"].order.hl7Order[2]
       = base64.enc(HL7)
   return P 
end

I used a simple approach of defining an XML template and then slotting in the base64 encoded message. From there it’s a matter of calling the web service:

function PlaceOrder(HL7, User, Password) 
   local E = MakePayload(HL7)
   trace(E)
   local D = net.http.post{
         url=Url, 
         auth={username=User, password=Password},
         headers={['Content-Type']='text/xml',
         SOAPAction=Url..'/submitOrder'}, 
      body=E:S(), live=true}
   return xml.parse{data=D}
end

It wasn’t all that hard to get that working, the since Translator gives us immediate feedback with the error messages; which made it simple to figure out the required Content-Type and SOAPAction headers.

The next step is to decipher what we got back, there are three of possibilities:

  • A SOAP fault response if you really screw things up like sending an empty string instead of an HL7 order message
  • Or a normal response, containing a valid HL7 message
  • A normal response, containing error information related to the HL7 message

When the Quest interface returns an error we need to identify whether it’s BEA logic stuff or the core system. It’s a rather leaky abstraction! So far the following code seems to be adequate to figure out what type of response we have and extract error messages in a useful manner:

-- Returns nil if we do not have a SOAP fault response
function IsFault(X)  
   return X["env:Envelope"]["env:Body"]["env:Fault"] 
end

-- Extracts the response message from a non fault response
function GetResponse(X)
   return X["env:Envelope"]["env:Body"]
    ["m:submitOrderResponse"].result.responseMsg[2]:S()
end

-- Extracts the error message from a fault response
function GetFault(X)
   return X["env:Envelope"]["env:Body"]["env:Fault"].
      detail["bea_fault:stacktrace"][2]:S()
end

Code [top]

We packaged the code in a ‘quest’ shared module to make a nice clean API to work with.

This is how the Translator is meant to be used, by abstracting the complexity of the underlying plumbing into a shared into a shared modules. However when something goes wrong we can easily get under the hood to see what is going wrong. This is really important when dealing with a complex and ambiguous protocol like SOAP, because there will always be problems.

Here’s the code for the main:

require('node')
require('quest')

local User ='user'

local Password = 'password'

local function trace(a,b,c,d) return end

function main(HL7)
   local X = quest.PlaceOrder(HL7,
      User, Password)
   trace(X)

   if quest.IsFault(X) then
      quest.GetFault(X)
   else
      quest.GetResponse(X)    
   end
end

Here’s the code for the ‘quest’ module:

quest ={}

local function trace(a,b,c,d) return end

local Url = 'https://cert.hub.care360.com/orders/service'

T = [==[<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ord="http://medplus.com/orders">
   <soapenv:Header/>
   <soapenv:Body>
      <ord:submitOrder soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
         <order xsi:type="java:Order" xs:type="type:Order" xmlns:java="java:com.medplus.serviceHub.orders.webservice" xmlns:xs="http://www.w3.org/2000/XMLSchema-instance">
            <hl7Order xsi:type="xsd:base64Binary">${globalCleanHL7}</hl7Order>
         </order>
      </ord:submitOrder>
   </soapenv:Body>
</soapenv:Envelope>]==]

local function MakePayload(HL7)
   local P = xml.parse{data=T}
   P["soapenv:Envelope"]["soapenv:Body"]["ord:submitOrder"].order.hl7Order[2]
       = filter.base64.enc(HL7)
   return P 
end

function quest.PlaceOrder(HL7, User, Password) 
   local E = MakePayload(HL7)
   trace(E)
   local D,R = net.http.post{
         url=Url, 
         auth={username=User, password=Password},
         headers={['Content-Type']='text/xml',
         SOAPAction=Url..'/submitOrder'}, 
      body=E:S(), live=true}
   if (R == 401) then
      error('Looks like the user name and password were wrong.')
   end
   return xml.parse{data=D}
end

-- Returns nil if we do not have a SOAP fault response
function quest.IsFault(X)  
   return X["env:Envelope"]["env:Body"]["env:Fault"] 
end

-- Extracts the response message from a non fault response
function quest.GetResponse(X)
   return X["env:Envelope"]["env:Body"]
    ["m:submitOrderResponse"].result.responseMsg[2]:S()
end

-- Extracts the error message from a fault response
function quest.GetFault(X)
   return X["env:Envelope"]["env:Body"]["env:Fault"].
      detail["bea_fault:stacktrace"][2]:S()
end

Next steps [top]

Looking at how this interface has been implemented I would expect that there will be a fairly long burn in time to find all the edge cases.

Fortunately this is the kind of problem that Iguana is designed for since it gives comprehensive logging, and the ability to take failed transactions and validate them against the web service APIs and fix the problems.

Leave A Comment?