Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member

Hello again for another in the series of blogs that we (chris.paine and Joanna Chan) have been putting together about our experiences and learnings in building a cross platform mobile app using the SAP NetWeaver Cloud as a platform. This post will cover one issue that we spent a good morning banging our heads together solving.

Back to basics - what are we attempting to do?

First a little diagram to explain how hybrid mobile apps work (at least in respect to the bits we are talking about here.)

As per usual, chris.paine has put together a video explaining the whole concept, so if you've developed a liking for them, you can see it here:

Within our app we have some HTML pages and JavaScript which are doing most of the heavy lifting of the app. (There are some hooks into the native controls, but mainly it is HTML, CSS and JavaScript). These HTML pages are already within our application, they are not sourced from the cloud/web - that's a web app (which also has its place). So when we make a call from our JavaScript to the cloud we are technically doing a cross-domain request. That is, the domain of our HTML (local to phone) and the domain of the data we are trying to retrieve (in the cloud) are different.

Now because there are nasty people out there who would try to steal your info, or hi-jack your accounts to do nasty stuff, the web community came up with a simple rule. A web page can only ask for information from the same domain that it came from. This stops things like a malicious sites pretending to be linked with Facebook by embedding details from Facebook and getting you to click/enter things that you shouldn't.

Of course, when the HTML is stored locally on the phone, it certainly does have a different domain when compared to the cloud where the data should be coming from...

So when you try to run the code locally in the iPhone simulator, or even in your Chrome browser - you get an error. The page just doesn't return anything.

In the iPhone simulator, this problem was especially baffling as we didn't get any helpful return codes or texts to tell us that something had gone wrong due to an incorrect cross-domain request. When the ajax call was sent, all that was returned was a return code 0 (which in SAP land is usually a good thing :smile: ), and no text. The only way we figured out the source of the problem was to run the application in Chrome or another web browser with web inspector capabilities to analyze the network calls.

Trying it out in Chrome

Digging into the responses in Chrome this is a bit confusing:

The PUT request that is in our JavaScript is making an OPTIONS call first! Which it didn't seem we asked for!

$.ajax({
  type: 'PUT',
  url: url,
  dataType: 'json',
  data: JSON.stringify(jsonData),
  beforeSend: function(xhr) {
    var requestAuth = "Basic " + encodeBase64(localStorage["PairingID"] + ":" + localStorage["PairingPWD"]);
    xhr.setRequestHeader("Authorization", requestAuth);
  },
  success: function(data) {
    $.mobile.hidePageLoadingMsg();
  },
  error: function(xhr, options, error) {
    $.mobile.hidePageLoadingMsg();
    alert(xhr.status);
    alert(error);
  }
});

And that OPTIONS call is successful, but Chrome says our call was "Cancelled"!

The key is that the request is being made for Host: chris.wombling.com whilst the Origin (where the JavaScript was sourced from) is a local file. Chrome rightly interprets this as a cross-domain request and refuses to serve it. But why did it make that OPTIONS call?

CORS and what it does.

CORS stands for Cross-Origin Resource Sharing. According to W3C:

User agents commonly apply same-origin restrictions to network requests. These restrictions prevent a client-side Web application running from one origin from obtaining data retrieved from another origin, and also limit unsafe HTTP requests that can be automatically launched toward destinations that differ from the running application's origin.

In user agents that follow this pattern, network requests typically include user credentials with cross-origin requests, including HTTP authentication and cookie information. This specification [CORS specification] extends this model in several ways:

  • A response can include an Access-Control-Allow-Origin header, with the origin of where the request originated from as the value, to allow access to the resource's contents.

The user agent validates that the value and origin of where the request originated match.

  • User agents can discover via a preflight request whether a cross-origin resource is prepared to accept requests, using a non-simple method, from a given origin.

This is again validated by the user agent.

  • Server-side applications are enabled to discover that an HTTP request was deemed a cross-origin request by the user agent, through the Origin header.

This extension enables server-side applications to enforce limitations (e.g. returning nothing) on the cross-origin requests that they are willing to service.

That's a bit technical, although we'll refer back to it, but for now so we can understand what is happening we'll go for a simpler explanation - Wikipedia:

Cross-origin resource sharing (CORS) is a mechanism that allows a web page to make XMLHttpRequests to another domain.[1] Such "cross-domain" requests would otherwise be forbidden by web browsers, per the same origin security policy. CORS defines a way in which the browser and the server can interact to determine whether or not to allow the cross-origin request.[2] It is more powerful than only allowing same-origin requests, but it is more secure than simply allowing all such requests.

So the clever folks who make up our web standards have come up with a way to allow cross-domain requests. Chrome is one of the browsers that implements this restriction quite strictly (chris.painethe iPhone simulator is another one :wink: )The technical language in the W3C definition explains why the OPTIONS call is made. It is a "preflight" request - that is to say, before any data is requested the client checks from the server if it is allowed to get the data.

In the response that we got from the OPTIONS call, we can see that the returned header is very simple.

HTTP/1.1 200 OK

Allow: GET, HEAD, PUT, TRACE, OPTIONS

Content-Length: 0

Date: Wed, 06 Feb 2013 01:19:22 GMT

Server: SAP


Chrome(our client) checks this header for any CORS specific details, finds none and therefore refuses to proceed cancelling the request.


So what to do?

Well, we need to add some headers to our server response to flag to Chrome that it supports CORS.

In our Java servlet:

@Override
protected void doOptions(HttpServletRequest request,
                         HttpServletResponse response) throws ServletException, IOException {
     // TODO Auto-generated method stub
     super.doOptions(request, response);
     AllowCORS.addCORSHeaders(request, response, "GET, PUT");
}

and that AllowCORS.addCORSHeaders method:

public static void addCORSHeaders(HttpServletRequest request, HttpServletResponse response, String allowedVerbs)
{
     response.addHeader("Access-Control-Allow-Origin", "*");
     response.addHeader("Access-Control-Allow-Methods", allowedVerbs +", OPTIONS"); 
     String requestCORSHeaders = request.getHeader("Access-Control-Request-Headers");
     if (requestCORSHeaders != null)
     {
         response.addHeader("Access-Control-Allow-Headers", requestCORSHeaders);
     }
}

So what is happening here is that we are adding a few headers to the response.

We also need to tell jQuery that we're going to be doing CORS requests (do this on loading of the document):

$.support.cors = true;
$.mobile.allowCrossDomainPages = true;

Let's see what happens now in Chrome:

One problem overcome, and now another! Options is working, but PUT has cancelled!

Although our OPTIONS header now looks like:

HTTP/1.1 200 OK

Allow: GET, HEAD, PUT, TRACE, OPTIONS

Access-Control-Allow-Origin: *

Access-Control-Allow-Methods: GET, PUT, OPTIONS

Access-Control-Allow-Headers: accept, origin, authorization, content-type

Content-Length: 0

Date: Wed, 06 Feb 2013 03:07:21 GMT

Server: SAP

The PUT request headers need CORS headers too:

so adding that to the PUT method:

protected void doPut(HttpServletRequest request,
                     HttpServletResponse response) throws ServletException, IOException {
  AllowCORS.addCORSHeaders(request, response, "GET, PUT");
  try {
     DeviceSettings settings = authoriseAndValidate(request);
...

And we can successfully complete output:

Note: This test was done with an older version of our code which has now been updated to send data in JSON format.

Now you think that getting to this point means that it's going to work. Oh if only that were so. The final and most important thing is to specify in your app that your ajax call needs to support cross-domain. This is particularly important for iOS because if you forget to do that, all your hard work on the server side will be wasted. You just need this little line "crossDomain: true" in all your ajax calls to make it work:

$.ajax({
  type: 'PUT',
  url: url,
  dataType: 'json',
  data: JSON.stringify(jsonData),
  crossDomain: true,
  beforeSend: function(xhr) {
    var requestAuth = "Basic " + encodeBase64(localStorage["PairingID"] + ":" + localStorage["PairingPWD"]);
    xhr.setRequestHeader("Authorization", requestAuth);
  },
  success: function(data) {
    $.mobile.hidePageLoadingMsg();
  },
  error: function(xhr, options, error) {
    $.mobile.hidePageLoadingMsg();
    alert(xhr.status);
    alert(error);
  }
});

Of course, testing between platforms uncovered that Android didn't necessarily need that little line to work (chris.paine yes I know I know), but for consistencies sake it was put in. Now it's all good!

In summary, key learnings

  • CORS is great, but you have to ensure that you set up your server to use it.
  • On the server, always put the logic to allow CORS at the beginning of your servlet, because if it doesn't get executed and you return a 404 or something like that, the client will never even see your response.
  • It is worth considering when building your code, that if the client fails to verify CORS, then you might have the request processed, but the client unaware of it happening.
  • Chrome is darn picky compared to Firefox (which is a bit of a floozy when it comes to deciding what local files to force same-origin policy on.)
  • If you can get your app working running as a local file (just running the HTML directly) in Chrome, chances are it will work in the simulators, which aren't quite as picky.

Disclaimer: As usual, all code here is for use at your own risk, with no promises it won't wreck your system :smile: . All opinions are strictly ours only and do not reflect on our employer. Please rate if you like it and share the love.

4 Comments
Labels in this area