Search code examples
c#xmlxml-deserializationasp.net-core-6.0

XML-based string is failing to post due to DOCTYPE line


I created a C# class based on the test file using paste special -> XML to class. That worked fine, structurally all seems good. However, I am running into an issue where if I paste this into Swagger in the test section, it'd error with 400 saying the xml field is required. After a lot of testing, I found that if I stripped out the <! DOCTYPE line, it worked fine.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.014/cXML.dtd">
<cXML xml:lang="en-US" payloadID="1452186890.009162@ip-10-7-14-126" timestamp="2008-01-07T09:14:50-08:00">
   ...
</cXML>

Source for the test file from Coupa

What do I need to do to allow it to go through as is? This XML is being posted in the body of the request so I don't think I can pre-scrap the <!DOCTYPE line (and frankly, may not be best to as want to dump the entire string into a local file anyway). the cXML is the parent element so I don't think it is as simple as adding a docType object to it and calling it a day as they're siblings and not nested in some manner.

[HttpPost(Name = "PostPurchaseOrder")]
public ContentResult Post([FromBody]XElement xml)
{
  cXML cxml = new cXML();
  var serializer = new XmlSerializer(typeof(cXML));
  using (var reader = new StringReader(xml.ToString()))
  {
    cxml = (cXML)serializer.Deserialize(reader);
  }
  ...
}

Here is the request per Swagger UI:

curl -X 'POST' \
'https://localhost:7200/PurchaseOrder' \
-H 'accept: */*' \
-H 'Content-Type: application/xml' \
-d '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM 
"http://xml.cxml.org/schemas/cXML/1.2.014/cXML.dtd">
<cXML xml:lang="en-US" payloadID="1452186890.009162@ip-10-7-14-126" 
timestamp="2008-01-07T09:14:50-08:00">
 <Header>
   <From>
     <Credential domain="duns">
        <!-- Customer domain typically provided by Supplier, typically DUNS -->
        <Identity>dell</Identity>
        <!-- Customer id typically provided by Supplier, string -->
     </Credential>
  </From>
  <To>
     <Credential domain="Duns">
        <!-- Supplier domain typically provided by Supplier, typically DUNS -->
        <Identity>128293714</Identity>
        <!-- Supplier id typically provided by Supplier, string -->
     </Credential>
  </To>
  <Sender>
     <Credential domain="duns">
        <Identity>dell</Identity>
        <!-- same as From -->
        <Identity>dell</Identity>
        <!-- same as From -->
     </Credential>
     <UserAgent>Coupa Procurement 1.0</UserAgent>
     <!-- does not change -->
  </Sender>
 </Header>
 <Request deploymentMode="production">
  <OrderRequest>
     <OrderRequestHeader orderID="6112" orderDate="2008-01-07T09:14:50-08:00" type="new">
     <!-- Coupa supports "new" and "update" -->
        <Total>
           <Money currency="USD">1505.0</Money>
           <!-- Currency code configured in Coupa -->
        </Total>
        <ShipTo>
           <Address isoCountryCode="US" addressID="3119">
              <Name xml:lang="en">jmadden</Name>
              <PostalAddress name="default">
                 <DeliverTo>j maddedn</DeliverTo>
                 <Street>333 East Hill Dr</Street>
                 <City>san leandro</City>
                 <State>ca</State>
                 <PostalCode>22222</PostalCode>
                 <Country isoCountryCode="US">United States</Country>
              </PostalAddress>
              <Email name="default">jmadden@coupa1.com</Email>
           </Address>
        </ShipTo>
        <BillTo>
           <Address isoCountryCode="US" addressID="142">
              <Name xml:lang="en">SOB1</Name>
              <!-- Company Name under Company Information in Coupa -->
              <PostalAddress name="default">
                 <DeliverTo>Noah Sanity Attn: Noah Noah</DeliverTo>
                 <Street>3420 Flatiron Way</Street>
                 <City>West Index</City>
                 <State>NJ</State>
                 <PostalCode>43023</PostalCode>
                 <Country isoCountryCode="US">United States</Country>
              </PostalAddress>
           </Address>
        </BillTo>
        <Contact role="endUser">
           <Name xml:lang="en">j maddedn</Name>
           <Email name="default">jmadden@coupa1.com</Email>
        </Contact>
        <Comments xml:lang="en">header comment goes here if entered by user</Comments>
     </OrderRequestHeader>
     <ItemOut quantity="1" lineNumber="1">
        <ItemID>
           <SupplierPartID>223-4511</SupplierPartID>
           <!-- Coupa Item Part Number -->
           <SupplierPartAuxiliaryID>1005379527029\1</SupplierPartAuxiliaryID>
           <!-- Auxiliary Part Number is optional, typically used by punchout suppliers -->
        </ItemID>
        <ItemDetail>
           <UnitPrice>
              <Money currency="USD">1505.0</Money>
              <!-- Currency code configured in Coupa -->
           </UnitPrice>
           <Description xml:lang="en">OptiPlex 755 Energy Smart Minitower;IntelREG CoreTM 2 Quad Processor Q6600 (2.40GHz, 2X4M, 1066MHz FSB)</Description>
           <UnitOfMeasure>EA</UnitOfMeasure>
           <Classification domain="UNSPSC">44000000</Classification>
           <!-- Future expansion -->
        </ItemDetail>
        <Distribution>
           <Accounting name="bbbb">
              <!-- Coupa Account name -->
              <Segment id="bbb" description="ORG" type="Organization" />
              <Segment id="b" description="DEPT" type="Department" />
              <Segment id="bb" description="PROJ" type="Project" />
           </Accounting>
           <Charge>
              <Money currency="USD">1505.0</Money>
           </Charge>
        </Distribution>
        <Comments xml:lang="en">line item comment goes here if entered by user</Comments>
     </ItemOut>
  </OrderRequest>
 </Request>
 </cXML>'

And here is the response per Swagger UI:

{
 "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
 "title": "One or more validation errors occurred.",
 "status": 400,
 "traceId": "00-long-string-00",
 "errors": {
  "": [
    "An error occurred while deserializing input data."
  ],
  "xml": [
    "The xml field is required."
  ]
 }
}

Solution

  • You could add a custom input formatter:

    public class XElementInputFormatter : XmlSerializerInputFormatter
    {
        public XElementInputFormatter(MvcOptions options) : base(options)
        {
            SupportedMediaTypes.Add("application/xml");
        }
    
        protected override bool CanReadType(Type type)
        {
            if (type.IsAssignableFrom(typeof(XElement)))
            {
                return true;
            }
    
            return base.CanReadType(type);
        }
    
        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            var xmlDoc = await XDocument.LoadAsync(context.HttpContext.Request.Body, LoadOptions.None, CancellationToken.None);
    
            return InputFormatterResult.Success(xmlDoc.Root);
        }
    }
    

    Then register it in your services collection:

    builder.Services.AddControllers(options =>
        {
            options.InputFormatters.Insert(0, new XElementInputFormatter(options));
        });
    

    You could probably write a cXMLInputFormatter and skip the the extra step of using XDocument/XElement and just get the cXML directly from the body in the action.

    public ContentResult Post([FromBody] cXML xml) {}