Spark API and CORS

Ok… I’ve been beating my head against the wall for the last couple days trying to understand CORS as it pertains to my web app which is basically a Node.js server running Express, Jade and Stylus that dishes up a client app that uses AngularJS to help simplify the ajax type stuff, and make data binding easier.

I keep trying to do something like this and I think I’m starting to understand why it may be failing:

$http.post('https://api.sprk.io/v1/devices/elroy',{"message":"winning"}).
success(function (data, status, headers, config) {
  $scope.name = data.name;
}).
error(function (data, status, headers, config) {
  $scope.name = 'Error!'
});

Basically the above http post sends the Request Method: OPTIONS instead of POST. It does this because the browser knows it’s a cross domain request and it basically alerts the server to this fact. The server then needs to dish up some response that it’s ok to send various requests (GET, POST, OPTIONS, DELETE, etc). And I’m guessing the Spark API doesn’t do this yet. What’s interesting is that when you communicate directly from another server using Node.js http requests it just works, because it’s not a browser trying to implement security.

Here’s a nice short video on what the issue is more or less:

If the Spark API supported this OPTIONS request, we could do ajax calls directly from client side apps, and use frameworks like AngularJS to talk to the Spark Cloud. At least I think this is what needs to be done.

Another method I am considering… is make the client/server app implement a same-domain API (basically in the background ajax calls to the server app) that then forwards the requests to the Spark API.

Any thoughts would be helpful :slight_smile:

Hi @BDub,
I’ve found a very good tutorial about cors. There is an option to make “simple requests” without the first preflight request if

  • The HTTP Method matches [HEAD, GET, POST]
  • Content-Type matches [application/x-www-form-urlencoded, multipart/form-data, text/plain]

(for detailed information see the section “Types of CORS requests”)

So - without ever tried it - I think if you modify your request there is no need to modify the server.

Nevertheless: If you are accessing the SparkCloud from the client’s browser, you have to provide the authorisation credentials to that client… so until your website is not else secured, everyone can access your Core (and even load up a new program??).

That was exactly the last thing I read at 1:30am :-). If you keep reading though you’ll see that there is no way to fake out the browser into sending a simple request. The browser detects that you are communicating cross-domain and changes your simple request to a preflight OPTIONS request. It’s a security issue. There is a way around it but it does make the whole web app more complicated as I mentioned, using an in-origin API scheme.

Thanks for the help! Lets keep thinking about this as a lot of people can benefit from it.

Edit: yeah actually that is right apparently if you use plain text it might work. I kind of like the JSON better though and I haven’t got plain text to work. Ill try that.

Ok it really helps to NOT be coding at 1:30am and to re-read stuff when you have a proper amount of coffee in you.

Re-reading Spark API docs: “You may send request data either web form encoded or in JSON. With curl’s -d flag, the default encoding is application/x-www-form-urlencoded, but some data structures are hard to represent that way.”

So I was trying plain text like a dummy a few times and went back to trying to make JSON work because I have that working in Node already. I also tried x-www-form-urlencoded but had extra headers that were making Chrome think I really WAS trying to communicat cross-domain, so simpler is better.

x-www-form-urlencoded works and it’s not too bad actually for the simpler stuff I’m doing right now. I’m sure it could get more complicated later and maybe we’ll need to revisit CORS then, but for now this is working in Angular:

$http({
  url: "https://api.sprk.io/v1/devices/elroy",
  method: "POST",
  data: "pin=D0&level=HIGH",
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
}).success(function(response) {  
  $scope.response = response;
  console.log(JSON.stringify(response, null, ' '));
}).error(function(error) {
  $scope.error = error;
});

Thanks for kicking me in the right direction again @dominikkv :smile:

Also, just FYI, the Spark API already supports CORS. If you run the following from your terminal:

curl -v https://api.sprk.io/v1/devices/elroy -d message=winning

You’ll see the following header in the response:

Access-Control-Allow-Origin: *

In fact, the other day I made a quick little pin twiddler that requires it.

Hey that’s pretty neat and you beat me to it! I’m still working on my webapp (not a web guy btw)… I’m trying to make it all pretty and stuff though… which is hard to do since I’m googling lots of it. I know what I want it to do, and I just beat my head against the keyboard and splash coffee in my eyes until it works.

Tonight was the first night I’ve tried “curl” from git bash on windows. It works! lol

I’m not going to argue with you if you say it supports CORS, but I’m thinking it’s not quite working fully. Try sending it a JSON encoded data as POST and see what happens when the server gets the OPTIONS request type. I’ve been seeing it just respond with 404 not found.

Nope, you’re absolutely right that it doesn’t respond to the pre-flight OPTIONS request. I’ll add that tonight, it’ll just take a few minutes. Not all libraries actually perform the pre-flight—jQuery doesn’t—but it’s easy to add so there’s no need for us to trip you up there.

Thanks! I’m a complete noob at CORS but I’ve been reading a lot lately and my understanding is the various libraries and frameworks craft the initial headers… and the browsers that support CORS convert your requests… and do things like change a POST to OPTIONS because you are operating cross-domain. I could be completely wrong about that… trying to learn though.

Added OPTIONS response with Access-Control-Allow-Origin: * and Access-Control-Allow-Headers: X-Requested-With headers. Let me know if you have any problems.

@zachary I’m getting an OK now in plain text for just about everything I send as OPTIONS request type… but not the JSON ok.

If I use $http.post and leave off any additional headers, chrome is complaining that my content-type is not valid. “Request header field Content-Type is not allowed by Access-Control-Allow-Headers”. In the background angular’s $http method is defaulting to application/json content type. Actually if I force it to text/plain it works and returns the JSON object, even though what I sent was really JSON format (as a string).

I think we really want to make it work with JSON right?

You might benefit from looking at this server setup (checkout the 3:35 mark here of this video)

1 Like

Thanks @BDub for the video reference. The OPTIONS response doesn’t need any response body at all, so I changed from 200 response status (which sent the OK automatically) to 204, which is explicitly no response body. Also, the Access Control headers are now:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: X-Requested-With, Content-Type, Accept Access-Control-Max-Age: 300 Access-Control-Allow-Origin: *

Let me know if you have any problems—I think the whole flow ought to work fine for you now.

2 Likes

I tested the following two methods and they work perfectly now. They actually show up in the browser (Chrome and Firefox) as a POST request type, which means the OPTIONS preflight took place in the background successfully :smile:

  // AngularJS
  $http({
    url: "https://api.sprk.io/v1/devices/elroy",
    method: "POST",
    data: {"pin":"D0","level":"HIGH"},
    headers: {
      'Content-Type': 'application/json'
    }
  }).success(function(response) {  
    $scope.response = response;
    console.log(JSON.stringify(response, null, ' '));
  }).error(function(error) {
    $scope.error = error;
  }

  // Shorthand way
  $http.post('https://api.sprk.io/v1/devices/elroy', 
  {"pin":"D0","level":"HIGH"}).
  success(function(response) {  
    $scope.response = response;
    console.log(JSON.stringify(response, null, ' '));
  }).error(function(error) {
    $scope.error = error;
  });

Now I can have my cake and eat it too… :heart: JSON

Thanks for making this work right!