0
votes

I have a .NET Core 2.1 Web API controller and method that is supposed to consume POST XML requests from an external service via HTTP. Below is the method header of the controller action.

    [HttpPost]
    [Produces("application/xml")]
    public async Task<IActionResult> PostReceivedMessage([FromBody] ReceivedMessage receivedMessage)

I wrote up a custom XML input formatter to process the XML request that works just fine when I post a sample XML request from Postman to the app's controller action. But when the service sends a similar request, the response from the app has the status 400, Bad Request.

After some debugging, I discovered that the requests come in with

Content-Type: application/x-www-form-urlencoded

instead of application/xml or text/xml as one would likely expect. The same behaviour is exhibited by the app if I change the header to match the content-type in the request sent by the external service.

I assume that x-www-form-urlencoded is meant for form data because model binding doesn't work when I change the action header to:

public async Task<IActionResult> PostReceivedMessage([FromForm] ReceivedMessage receivedMessage)

Since I have no control over the external service, how should I make the controller action able to process XML requests with x-www-form-urlencoded as the content-type?

UPDATE: Below is a sample request:

POST /check/api/receivedmessages HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.7.0_45
Host: xxx.xxx.xxx.xxx
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-type: application/x-www-form-urlencoded
Content-Length: 270

<Request><requestId>95715274355861000</requestId><msisdn>345678955041</msisdn><timeStamp>2019/10/20 02:23:55</timeStamp><keyword>MO</keyword><dataSet><param><id>UserData</id><value>VHVqrA==</value></param><param><id>DA</id><value>555</value></param></dataSet></Request>
2
What does the received request look like? Can you post a sample of that? - JSteward
Use a sniffer like wireshark or fiddler and compare first request in postman the works with non working c# app. Then make the c# look exactly like the postman request. The default headers from c# are not the same as postman and making the c# request look like postman will solve issue. - jdweng
@JSteward Updated the post to include a sample. - Steve S
@jdweng Already used Wireshark to compare the two requests. That's how I found out it that the bad request status is only returned when with Content-type: application/x-www-form-urlencoded and not with Content-type: application/xml. - Steve S
Get the XML, not the x-www-form-urlencoded. You should be able to get the xml as a string from the response. Then process the xml string using XML methods. - jdweng

2 Answers

0
votes

It is simple to get string and convert xml string to C# object:

[HttpPost]
[Produces("application/xml")]      
public async Task<IActionResult> PostReceivedMessage([FromForm]string receivedMessage)
    {

        XmlSerializer serializer = new XmlSerializer(typeof(ReceivedMessage));
        ReceivedMessage data;
        using (TextReader reader = new StringReader(receivedMessage))
        {
             data = (ReceivedMessage)serializer.Deserialize(reader);
        }


        return Ok(data);
    }
0
votes

I ended up taking @Xing Zou suggestion offered in the comments and implemented a custom model binder. I'm not sure if that's overkill but at least it keeps the controller action "lean".

The custom model binder looks like this:

public class ReceivedMessageEntityBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var request = bindingContext.HttpContext.Request;

            var firstKey = request.Form.Keys.First();
            StringValues formValue = "";

            request.Form.TryGetValue(firstKey, out formValue);

            var requestBody = firstKey + "=" + formValue;

            bindingContext.Result = ModelBindingResult.Success(FromXmlString(requestBody));

            return Task.CompletedTask;  
        }

        private ReceivedMessage FromXmlString(string requestBody)
        {
            XElement request = XElement.Parse(requestBody);

            var receivedMessage = new ReceivedMessage();

            receivedMessage.RequestId = (string)
                                        (from el in request.Descendants("requestId")
                                         select el).First();

            receivedMessage.Msisdn = (string)
                                        (from el in request.Descendants("msisdn")
                                         select el).First();


            receivedMessage.Timestamp = DateTime.Parse(
                                        (string)
                                        (from el in request.Descendants("timeStamp")
                                         select el).First());


            receivedMessage.Keyword = (string)
                                        (from el in request.Descendants("keyword")
                                         select el).First();


            IEnumerable<XElement> dataSet = from el in request.Descendants("param")
                                            select el;

            foreach (var param in dataSet)
            {
                var firstNode = param.Descendants().First();

                switch (firstNode.Value)
                {
                    case "UserData":
                        receivedMessage.UserData = (firstNode.NextNode as XElement).Value;
                        break;

                    case "DA":
                        receivedMessage.Da = (firstNode.NextNode as XElement).Value;
                        break;
                }
            }

            return receivedMessage;
        }
    }

And the model is now decorated to so that the custom model binder can be used to bind to it.

[ModelBinder(BinderType = typeof(ReceivedMessageEntityBinder))]
    public class ReceivedMessage
    {
        public long Id { get; set; }

        [StringLength(12)]
        public string Msisdn { get; set; }
        public string RequestId { get; set; }
        public DateTime Timestamp { get; set; }
        public string Keyword { get; set; }
        public string UserData { get; set; }
        public string Da { get; set; }
    }

One thing to note. The XML sent by the external service contains a value that is base64 encoded. This means that there are two "=" signs and my guess is that this leads the body to be interpreted as a form with 1 key and 1 value. For example:

[key]<Request><requestId>95715274355861000</requestId><msisdn>345678955041</msisdn><timeStamp>2019/10/20 02:23:55</timeStamp><keyword>MO</keyword><dataSet><param><id>UserData</id><value>VHVqrA
[value]=</value></param><param><id>DA</id><value>555</value></param></dataSet></Request>

Hence the reason for the way my model binder is awkwardly extracting the request body into a string.

I suppose if that were not the case, it's possible to get the body by just reading the first (and only key) in the form.