2

I've been trying for most of the day to post data to an external WebAPI endpoint when the OnSave event is triggered from a Microsoft Dynamics 365 form. Unfortunately, despite an extraordinary amount of research, I haven't been able to successfully post the data.

Having spent about an hour stumbling around getting the javascript library referenced in the right form, I tried reference jQuery from within my script, and trying to use $.ajax to trigger the post, using a declared alias. Here's a redacted version of the code:

function UpdateSupplierStatus()
{
   var $jQ = jQuery.noConflict();

   var MembershipNumber = 
Xrm.Page.data.entity.attributes.get("membershipnumber").getValue();
var StatusCode = Xrm.Page.data.entity.attributes.get("statuscode").getText();

$jQ.ajax({
    url: 'http://myurl.com/api/MembershipAPI/UpdateMembershipStatus', 
    type: 'POST',
    data: {
            'membershipNumber' : MembershipNumber,
            'statusCode' : StatusCode
        },
    datatype: 'json',
    success: function() {},
    error: alert("There was an error performing the update.")
});
}

No matter what I try, it seems that as soon as I try to execute the post, I fall straight into the 'error' clause. I've run Fiddler in the background whilst debugging the script using Internet Explorer, and there's no attempt made to hit the endpoint specified - it simply doesn't try, just errors.

I did some research, and came across a number of articles like this that suggest using XmlHttpRequest instead of trying to poke posts around the internet using jQuery, and so I tried using this instead:

function UpdateSupplierStatus()
{
var MembershipNumber = Xrm.Page.data.entity.attributes.get("membershipnumber").getValue();
var StatusCode = Xrm.Page.data.entity.attributes.get("statuscode").getText();

var Query = "?membershipNumber=" + MembershipNumber + "&statusCode=" + StatusCode;
var URL = "http://myurl.com/api/MembershipAPI/UpdateMembershipStatus";

var req = new XMLHttpRequest();
req.open("POST", URL + Query, true);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");

req.send(JSON.stringify(data));
}

Again, if I debug this it's unsuccessful, however this time it seems that the offending line is 'req.open(...' - from what I've read, the suggestion is that I can't use XmlHttpRequest to make requests to resources outside of the hosts domain. The problem here is that CRM is hosted and the related website is obviously elsewhere. There is a question around this problem that describes the 'fix' as requiring users to alter their security settings, but frankly I find this to be madness - I cannot, and will not, require my customers to alter their security settings to allow this to work.

What am I missing here? Is it really this difficult to post data to an external site using Dynamics 365, or is there some process that I'm missing that facilitates exactly this?

Edit So I've tried testing this with a different browser and I get further using the XmlHttpRequest method than I was getting with the $.ajax method. However, I've not self-signed localhost, and our staging environment has no SSL certificate, and so I can't supply the content via HTTPS. I now have two issues:

I can't require people to not use IE
I can't easily get HTTPS working in either my development environment, or in my staging environment

I'm starting to feel more and more like this isn't a problem with Dynamics!

Community
  • 1
  • 1
Ashilta
  • 173
  • 1
  • 10

2 Answers2

4

This is not an issue related to Dynamics 365 but related to CORS:

For security reasons, browsers restrict cross-origin HTTP requests initiated from within scripts. For example, XMLHttpRequest and Fetch follow the same-origin policy. So, a web application using XMLHttpRequest or Fetch could only make HTTP requests to its own domain.

The Cross-Origin Resource Sharing (CORS) mechanism gives web servers cross-domain access controls, which enable secure cross-domain data transfers. Modern browsers use CORS in an API container - such as XMLHttpRequest or Fetch - to mitigate risks of cross-origin HTTP requests.

More information...

You will find a lot of examples about different scenarios and different ways to perform a cross-domain request (in the previous link you will find a lot of these information).

The following it's a "simple request", which I think it will work in your scenario (the allowed methods are GET, HEAD and POST), and you can test it directly from CRM if you want (I've done it without problems as you can see in the screenshot that I've also uploaded):

function callOtherDomain() {
  var req = new XMLHttpRequest();
  var url = 'https://httpbin.org/get';
   
  req.open('GET', url, true);

  req.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
       alert(req.responseText);
    }
  };
  
  req.send(); 
}

enter image description here

This "simple request" will use CORS headers to handle the privileges:

enter image description here

As you can see, the server (http://myurl.com in your scenario) also has to be configured to explicitly allow cross-domain requests. You can see a very detailed tutorial to enable this option in the following MSDN article (assuming that you're using WebAPI).

Community
  • 1
  • 1
Federico Jousset
  • 1,661
  • 14
  • 21
  • Hi @federico-jousset - Thanks for replying, however I'm not convinced that this is a Cors issue for two reasons; firstly, I've tried this whilst accessing CRM via my PC and debugging the API endpoint on the same machine, changing the JS in CRM to point to Localhost appropriately. Thus, if I use the ajax.Post method, the origin and destination are within the same origin, but I see the same symptoms. Secondly, I've tried using your method, with an XmlHttpRequest and added EnableCors(origins: "*", headers: "*", methods: "*") to the attributes of the method - no joy. – Ashilta Apr 12 '17 at 12:47
  • It doesn't matter if they are in the same machine/hostname, they're probably using different ports so they're not considered to be in the same domain. Check https://tools.ietf.org/html/rfc6454#section-5: " If the two origins are scheme/host/port triples, the two origins are the same if, and only if, they have identical schemes, hosts, and ports.". In the attached MSDN article there's a section called "What is same origin?" that could be helpful. – Federico Jousset Apr 12 '17 at 12:55
  • Ok, fair point about the ports (one will invariably be port 80, and the API is most definitely not), however surely the EnableCors attribute that I've added should permit connections from across origins? – Ashilta Apr 12 '17 at 13:01
  • How have you configured your origins? "*"? Can you check in the browser console if the call is still being block by the browser or if you're getting a different error in the response? – Federico Jousset Apr 12 '17 at 13:43
  • public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Other configuration omitted config.EnableCors(new EnableCorsAttribute("*", "*", "*")); } } – Federico Jousset Apr 12 '17 at 13:46
0

Your jQuery ajax call is not working because you are not passing a function definition to your error handler, but rather executing some code (alert...) directly.

Try wrapping the code in an inline function definition like this:

$jQ.ajax({
 url: 'http://myurl.com/api/MembershipAPI/UpdateMembershipStatus', 
 type: 'POST',
 data: {
        'membershipNumber' : MembershipNumber,
        'statusCode' : StatusCode
    },
 datatype: 'json',
 success: function() {},
 error: function() { alert("There was an error performing the update."); }
});

Also, add this line in the beginning of your code (after the .noConflict):

$jQ.support.cors = true;

Furthermore, please make sure your webservice accepts Cross Origin Requests.To do so in your Web API project, make the following changes:

  • Install the Nuget Package Microsoft.AspNet.WebApi.Cors
  • Add the following attribute to your class:

    [EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "")] public class MembershipAPIController : ApiController

  • Add the following in your web.config under the tag:

    <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> <add name="Access-Control-Allow-Methods" value="*" /> </customHeaders> <httpProtocol>

The last step is needed to include both headers in the response. When these headers are missing, the response will be blocked by the browser. I'm pretty sure you're missing the xml-tags in your web.config.

Nathan
  • 1,865
  • 4
  • 19
  • 25
  • Hi @nathan - I've tried effecting both of the changes you've proposed to the ajax post, but unfortunately I'm getting exactly the same problem; the alert appears immediately that the script is attempted. – Ashilta Apr 12 '17 at 12:50
  • Could you provide a jsfidlle / plunkr? – Nathan Apr 12 '17 at 12:56
  • https://jsfiddle.net/5p6bbLws/ I've put the class containing the API endpoint in the HTML section - it's poorly formatted but hopefully it gives you what you need – Ashilta Apr 12 '17 at 13:05
  • ok, I've played around a bit, and found that what you were missing were both the EnableCors attribute and the web.config xml. I've updated your fiddle. – Nathan Apr 12 '17 at 14:21