AngularJS $resource makes HTTP OPTIONS request instead of HTTP POST for $save method
Asked Answered
H

2

24

I'm in the process of writing a simple library application to get ready for a larger project with AngularJS. After reading a lot online about using $resource to interact with a RESTful API, I decided that it would probably offer some time-saving and scaling benefits to implement it instead of using $http for each request. The problem is that for some reason (I'm no expert on CORS and the request is being sent cross-domain) when using the $save method my Node.js console shows:

OPTIONS /books 200 1ms - 161b 

Using the query() method works fine - the Node console shows:

GET /books 200 1ms - 228b

I've been stuck for several hours at this point, trying variations on the below but it always ends up being an OPTIONS request instead of POST (which is what it should be according to the Angular documentation) for the $save method.

AngularJS Web App

app.js

var libraryApp = angular.module('libraryApp', ['ngResource', 'ngRoute', 'libraryControllers']);

libraryApp.factory('$book', ['$resource', function ($resource) {

    return $resource('http://mywebserver\\:1337/books/:bookId', { bookId: '@bookId' });
}]);

controllers.js

var libraryControllers = angular.module('libraryControllers', []);

libraryControllers.controller('BookCtrl', ['$scope', '$book', function($scope, $book) {

    ...

    $scope.addBook = function () {
        var b = new $book;
        b.isbn = "TEST";
        b.description = "TEST";
        b.price = 9.99;
        b.$save();
    };
}]);

Node.js with Express REST API

app.js

var express = require('express'),
    books = require('./routes/books'),
    http = require('http'),
    path = require('path');

var app = express();

...

// enable cross-domain scripting
app.all('*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", req.headers.origin);
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    next();
});

// routing
app.get('/books', books.getAll);
app.get('/books/:isbn', books.get);
// This is what I want to fire with the $save method
app.post('/books', books.add);

http.createServer(app).listen(app.get('port'), function(){
    console.log('Express server listening on port ' + app.get('port'));
});

./routes/books.js

...

exports.add = function(req, res) {

    console.log("POST request received...");
    console.log(req.body.isbn);
};

Tried putting this line in my config function delete $httpProvider.defaults.headers.common["X-Requested-With"]; but no change.

I'm no Angular/Node pro but right now I'm thinking that it's something to do with it being cross domain and, like I said, I'm no expert on CORS.

Thanks in advance.

Hesky answered 19/1, 2014 at 19:42 Comment(1)
Check this postFlavour
H
43

I know it may be in bad taste to answer my own question but I figured out the problem a few days after posting this.

It all comes down to how browsers manage CORS. When making a cross-domain request in JavaScript that is not "simple" (i.e. a GET request - which explains why the query() function worked), the browser will automatically make a HTTP OPTIONS request to the specified URL/URI, called a "pre-flight" request or "promise". As long as the remote source returns a HTTP status code of 200 and relevant details about what it will accept in the response headers, then the browser will go ahead with the original JavaScript call.

Here's a brief jQuery example:

function makeRequest() {
    // browser makes HTTP OPTIONS request to www.myotherwebsite.com/api/test
    // and if it receives a HTTP status code of 200 and relevant details about
    // what it will accept in HTTP headers, then it will make this POST request...
    $.post( "www.myotherwebsite.com/api/test", function(data) {
        alert(data);
    });
    // ...if not then it won't - it's that simple.
}

All I had to do was add the details of what the server will accept in the response headers:

// apply this rule to all requests accessing any URL/URI
app.all('*', function(req, res, next) {
    // add details of what is allowed in HTTP request headers to the response headers
    res.header('Access-Control-Allow-Origin', req.headers.origin);
    res.header('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS');
    res.header('Access-Control-Allow-Credentials', false);
    res.header('Access-Control-Max-Age', '86400');
    res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
    // the next() function continues execution and will move onto the requested URL/URI
    next();
});

And then insert these few lines before the Express routing to simply return a HTTP 200 status code for every OPTIONS request:

// fulfils pre-flight/promise request
app.options('*', function(req, res) {
    res.send(200);
});

Hopefully this will help anyone who stumbles on this page suffering from the same problem.

Hesky answered 12/3, 2014 at 16:32 Comment(4)
It's not bad taste at all to answer your own question, in fact it's encouraged if you find the solution to the problem yourself.Foxed
I'm worried about extra http 'OPTONS' request. Is there a way to avoid OPTION request from angularjs?Paramagnet
Unfortunately not as it's done by the browser on CORS requests.Hesky
If you are not allowed to change the server implementation a possible workaround is to use chromium-browser with the option --disable-web-security. I tested it in Ubuntu, from any location: $ chromium-browser --disable-web-security &Mingle
D
1

I didn´t actually try this, but wouldn´t it be enough to tell the Ressource how to handle the $save request?

$resource('http://mywebserver\\:1337/books/:bookId', { bookId: '@bookId' }, {save: {method: 'POST'});
Debug answered 6/11, 2014 at 19:19 Comment(2)
No, the issue was because it was a cross domain request. Even if I'd have used the method you've provided, the browser would have still made the OPTIONS request.Hesky
Right, just created a plunker out of curiosity. Same here, POST gets transformed to OPTIONS.Debug

© 2022 - 2024 — McMap. All rights reserved.