Caynadian
Caynadian

Reputation: 779

Delphi 2007 SOAP Fault Processing

I am writing a SOAP client in Delphi 2007 to do a simple Customs release check. I send the SOAP server some information and I am supposed to either receive details back about the Customs release or a SOAP fault if the server could not locate the information I sent it. The first part works fine but processing of the fault does not. The WSDL specifies a custom SOAP exception (this is included by the main WSDL - the whole WSDL is not shown):

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsd:schema targetNamespace="http://trips.crownagents.com/wsexception/message"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns="http://trips.crownagents.com/wsexception/message">
  <xsd:element name="WSException" type="WSException" nillable="true"/>
  <xsd:complexType name="WSException">
    <xsd:sequence>
      <xsd:element name="ErrorCode" type="xsd:string" minOccurs="0" maxOccurs="1"/>
      <xsd:element name="ErrorDescription" type="xsd:string" minOccurs="0" maxOccurs="1"/>
      <xsd:element name="Stack" type="xsd:string" minOccurs="0" maxOccurs="1"/>
    </xsd:sequence>
  </xsd:complexType>
</xsd:schema>

And the SOAP response I get back seems to reference the exception:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" 
              xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xmlns:ns0="http://trips.crownagents.com/wsexception/message" 
              xmlns:ns1="http://trips.crownagents.com/external/customs/release/message" 
              xmlns:ns2="http://trips.crownagents.com/external/common/message">
  <env:Body>
    <env:Fault xsi:type="env:Fault">
      <faultcode>env:Server</faultcode>
      <faultstring xsi:nil="1"/>
      <detail>
        <ans1:WSExceptionResponse xmlns:ans1="http://msgsvr.trips.crownagents.com/">
          <ErrorCode>0002</ErrorCode>
          <ErrorDescription>Invalid Declaration</ErrorDescription>
          <Stack>getSingleResult() did not retrieve any entities.</Stack>
        </ans1:WSExceptionResponse>
      </detail>
    </env:Fault>
  </env:Body>
</env:Envelope>

But, my code never sees the WSExceptionResponse. Instead, I get a generic ERemotableException:

Try
  Res := Rel.releaseStatus(RelInfo);
Except
  On E: WSExceptionResponse Do  // This never fires
    Status('Release check error (' + E.ErrorCode + ' - ' +
           E.ErrorDescription + ').', True);
  Else
    Status('Release check error (' + Exception(ExceptObject).Message +
           ').', True);
End;

I have read that there are a couple of issues with SOAP processing in Delphi 2007 (https://groups.google.com/forum/#!msg/borland.public.delphi.webservices.soap/71t3P-vPMbk/qw9JVTEVS3YJ) and I have changed the OPToSOAPDomConv.pas file to revert it as per the suggestion but that doesn't help. Does anyone have any ideas as to what I might be doing wrong?

Upvotes: 3

Views: 1877

Answers (1)

Caynadian
Caynadian

Reputation: 779

For anyone else still using Delphi 2007 that comes across this question, this is how I fixed this issue.

First, copy OPToSOAPDomConv.pas and InvokeRegistry.pas from the Delphi source directory (\Program Files< (x86)>\CodeGear\RAD Studio\5.0\source\Win32\soap) to your project directory. Add these two files to your project as you will be customizing the source and you will need these to recompile with your project instead of using the precompiled DCUs that come with Delphi.

In the OPToSOAPDomConv.pas file, find the ProcessFault procedure and replace it with the following:

procedure TOPToSoapDomConvert.ProcessFault(FaultNode: IXMLNode);
var
  FA, FC, FD, FS, CustNode: IXMLNode;
  I, J: Integer;
  AMessage: WideString;
  AClass: TClass;
  URI, TypeName: WideString;
  Count: Integer;
  PropList: PPropList;
  Ex: ERemotableException;

  function GetNodeURIAndName(const Node: IXMLNode; var TypeURI,
    ElemName: InvString): boolean;
  var
    Pre: InvString;
  begin
    ElemName := Node.NodeName;
    if IsPrefixed(ElemName) then
    begin
      Pre := ExtractPrefix(ElemName);
      ElemName := ExtractLocalName(ElemName);
      TypeURI := Node.FindNamespaceURI(Pre);
    end
    else
      TypeURI := Node.NamespaceURI;
    Result := True;
  end;

begin
  FA := nil;
  FC := nil;
  FD := nil;
  FS := nil;
  Ex := nil;
  for I := 0 to FaultNode.ChildNodes.Count - 1 do
  begin
    if      SameText(ExtractLocalName(FaultNode.ChildNodes[I].NodeName), SSoapFaultCode) then
      FC := FaultNode.ChildNodes[I]
    else if SameText(ExtractLocalName(FaultNode.ChildNodes[I].NodeName), SSoapFaultString) then
      FS := FaultNode.ChildNodes[I]
    else if SameText(ExtractLocalName(FaultNode.ChildNodes[I].NodeName), SSoapFaultDetails) then
      FD := FaultNode.ChildNodes[I]
    else if SameText(ExtractLocalName(FaultNode.ChildNodes[I].NodeName), SSoapFaultActor) then
      FA := FaultNode.ChildNodes[I];
  end;

  { Retrieve message from FaultString node }
  if FS <> nil then
    AMessage := FS.Text;

  { If there's a <detail> node, try to map it to a registered type }
  if FD <> nil then
  begin
    { Some SOAP stacks, including Delphi6 and others (see
      http://softwaredev.earthweb.com/script/article/0,,12063_641361_2,00.html)
      use the approach of putting custom fault info right at the <detail> node:

      Listing 4 - Application Fault Details
      <SOAP-ENV:Fault>
        <faultcode>300</faultcode>
        <faultstring>Invalid Request</faultstring>
        <runcode>1</runcode>
        <detail xmlns:e="GetTemperatureErr-URI"
                xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
                xsi:type="e:GetTemperatureFault">
            <number>5575910</number>
            <description>Sensor Failure</description>
            <file>GetTemperatureClass.cpp</file>
            <line>481</line>
        </detail>
      </SOAP-ENV:Fault>

      However, much more common is the approach where the type and namespace
      are on the childnode of the <detail> node. Apache, MS and the SOAP spec.
      seem to lean towards that approach:

      Example 10 from the SOAP 1.1 Spec:

      <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
         <SOAP-ENV:Body>
             <SOAP-ENV:Fault>
                 <faultcode>SOAP-ENV:Server</faultcode>
                 <faultstring>Server Error</faultstring>
                 <detail>
                     <e:myfaultdetails xmlns:e="Some-URI">
                       <message>My application didn't work</message>
                       <errorcode>1001</errorcode>
                     </e:myfaultdetails>
                 </detail>
             </SOAP-ENV:Fault>
         </SOAP-ENV:Body>
      </SOAP-ENV:Envelope>

      For interop reasons we favor the later approach but we'll support both here!!
    }
    CustNode := nil;
    if GetElementType(FD, URI, TypeName) then
      CustNode := FD
    else
    begin
      if ntElementChildCount(FD) > 0 then
      begin
        CustNode := ntElementChild(FD, 0);
        if not GetElementType(CustNode, URI, TypeName) and
           not GetNodeURIAndName(CustNode, URI, TypeName) then
          CustNode := nil;
      end;
    end;

    if (CustNode <> nil) then
    begin
      AClass := RemClassRegistry.URIToClass(URI, TypeName);
      if AClass <> nil then
      begin
        if AClass.InheritsFrom(ERemotableException) then
        begin
          Ex := ERemotableExceptionClass(AClass).Create(AMessage);
          LoadObject(Ex, FaultNode, CustNode);
        end;
      end;
    end;
  end;

  { Create default SOAP invocation exception if no suitable class was found }
  if Ex = nil then
    Ex := ERemotableException.Create(AMessage);
  if FA <> nil then
    Ex.FaultActor := FA.Text;
  if FC <> nil then
    Ex.FaultCode := FC.Text;
  if FD <> nil then
    Ex.FaultDetail := FD.XML;
  raise Ex;
end;

Next, find the GetElementType function and replace it with the following:

function TSOAPDomConv.GetElementType(Node: IXMLNode; var TypeURI, TypeName: InvString): Boolean;
var
  Idx: Integer;
  S : InvString;
  V: Variant;
  Pre: InvString;
begin
  TypeURI := '';
  TypeName := '';
  Result := False;
  if (Node.NamespaceURI = SSoap11EncodingS5) and
     (Node.LocalName = SSoapEncodingArray) then
  begin
    TypeURI := SSoap11EncodingS5;
    TypeName := SSoapEncodingArray;
    Result := True;
  end
  else
  begin
    V := GetTypeBySchemaNS(Node, XMLSchemaInstNameSpace);
    if VarIsNull(V) then
      V := Node.GetAttribute(SSoapType);
    if not VarIsNull(V) then
    begin
      S := V;
      Idx := Pos(':', S);  { do not localize }
      if Idx <> 0 then
      begin
        TypeName := Copy(S, Idx + 1, High(Integer));
        Pre := Copy(S, 1, Idx - 1);
        TypeURI := Node.FindNamespaceURI(Pre);
      end
      else
      begin
        TypeName := S;
        TypeURI := '';
      end;
      Result := True;
    end;
  end
end;

Finally, open the InvokeRegistry.pas file and find the GetExternalPropName function. Change the line that says:

if Info.Kind = tkClass then

to this:

if (Info.Kind = tkClass) and Assigned(GetTypeData(info).ParentInfo) then

Compile and run your application and you should be good.

All credit for this goes to the users in this thread http://www.codenewsfast.com/cnf/article/859054074/permalink.art-ng1920q2368 and this one http://www.delphigroups.info/2/7/342954.html.

Upvotes: 3

Related Questions