My problem is that HTTP headers are not being sent from my AngularJS HTTP GET requests. However, for a HTTP POST, I do see the headers being set. These HTTP requests are over CORS.
Although there are a lot of SO posts on this problem, I have tried them and none of them worked. One of the solutions suggests that HTTP headers are not sent if the data field is empty, and I've tried the suggestion to add an empty data value (which doesn't really sense for a HTTP GET request, by the way), but still, the HTTP headers do not make it.
On a side note, I might defend that this post/question may merit itself as "not a duplicate" (from the other SO posts) as it deals with HTTP GET (as opposed to HTTP POST) and CORS (as opposed to not-CORS).
Here is my technology stack.
To enable CORS, I followed the example here http://enable-cors.org/server_expressjs.html. My NodeJS server application looks like the following.
var express = require('express');
var bodyParser = require('body-parser');
var morgan = require('morgan');
var jwt = require('jsonwebtoken');
var port = process.env.PORT || 8080;
var router = express.Router();
var app = express();
app.set('secret', 'mySecret');
app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(morgan('dev'));
app.use('/api', router);
router.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-access-token');
res.header('Access-Control-Allow-Methods', 'POST, PUT, GET, OPTIONS, DELETE');
next();
});
router.post('/authenticate', function(req, res) {
var username = req.body.username;
var pw = req.body.password;
if(username !== 'root') {
res.json({
success: false,
message: 'User not found'
});
} else if(pw !== 'root') {
res.json({
success: false,
message: 'Password wrong'
});
} else {
var user = {
username: username,
pw: pw
};
var token = jwt.sign(user, app.get('secret'), {
expiresIn: 60 * 60 * 24 * 365
});
res.json({
success: true,
message: 'Enjoy your token!',
token: token
});
}
});
router.use(function(req, res, next) {
/* according to comments, have to ignore OPTIONS request from protection */
if('OPTIONS' === req.method) { next(); return; } //original post modified here to show, after adding this line, the OPTIONS is accessible, then the GET does actually send the required HTTP header
if('/api/authenticate' === req.originalUrl) {
next();
return;
}
var token = req.body.token || req.params['token'] || req.headers['x-access-token'];
if(token) {
jwt.verify(token, app.get('secret'), function(err, decoded) {
if(err) {
return res.json({
success: false,
message: 'Failed to authenticate token'
});
} else {
req.decoded = decoded;
next();
}
})
} else {
return res.status(403).send({
success: false,
message: 'No token provided'
});
}
});
router.get('/users', function(req, res) {
res.json([
{ fname: 'john', lname: 'doe' },
{ fname: 'jane', lname: 'smith' }
]);
})
var server = app.listen(port, function() {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
My AngularJS service looks like the following.
myServices.factory('HomeService', ['$resource', '$http', '$location', '$cookies', 'conf', function($resource, $http, $location, $cookies, conf) {
var svc = {};
svc.getRestUrl = function() {
return 'http://localhost:8080';
};
svc.sendData = function(url, data, method) {
var restUrl = svc.getRestUrl() + url;
var options = {
method: method,
url: restUrl,
withCredentials: false
};
var token = $cookies.get('token');
if(_.isEmpty(token)) {
options.headers = {
'X-Requested-With': 'XMLHttpRequest'
};
} else {
options.headers = {
'X-Requested-With': 'XMLHttpRequest',
'x-access-token': token
};
}
if(data) {
options.data = data;
} else {
options.data = '';
}
return $http(options);
}
svc.getData = function(url) {
return svc.sendData(url, null, 'GET');
};
svc.postData = function(url, data) {
return svc.sendData(url, data, 'POST');
};
svc.authenticate = function(username, password) {
var data = JSON.stringify({
username: username,
password: password
});
return svc.postData('/api/authenticate', data);
};
svc.getUsers = function() {
return svc.getData('/api/users');
};
return svc;
}]);
Note
authenticate
method, this is a HTTP POST getUsers
, this a HTTP GET data: ''
Using Fiddler, for authenticate
I see the following HTTP request.
POST http://localhost:8080/api/authenticate HTTP/1.1 Host: localhost:8080 Connection: keep-alive Content-Length: 37 Accept: application/json, text/plain, */* Origin: http://localhost X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36 Content-Type: application/json;charset=UTF-8 Referer: http://localhost/ Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.8 {"username":"root","password":"root"}
For getUsers
, I see the following HTTP request.
OPTIONS http://localhost:8080/api/users HTTP/1.1 Host: localhost:8080 Connection: keep-alive Access-Control-Request-Method: GET Origin: http://localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36 Access-Control-Request-Headers: accept, x-access-token, x-requested-with Accept: */* Referer: http://localhost/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8
Am I missing something here with regards to HTTP GET over CORS that the HTTP headers are not being sent?
According to your HTTP request, an OPTIONS request is fired to your CORS API icase of your getUsers
method.
When it comes to CORS, there are 2 kinds of requests
Simple requests
A simple cross-site request is one that meets all the following conditions:
The only allowed methods are:
Apart from the headers set automatically by the user agent, the only headers which are allowed to be manually set are:
The only allowed values for the Content-Type header are:
Preflighted requests
In case you make any request which violates the conditions of a simple request, then a "preflighted" OPTIONS request is sent in order to determine whether the actual request is safe to send.In particular, a request is preflighted if:
For more details about Preflighted Requests, you can refer to this MDN link.
I believe this is what is happening in your case. Even though you're making a simple GET request, you're adding 2 custom headers X-Requested-With
& x-access-token
which makes it necessary to validate the safety of your API, so a preflighted OPTIONS request is sent by the browser. The browser will continue with your GET request only if it receives valid response.
In your NodeJS server code, you're handling only POST requests to /authenticate
and GET requests to /users
, so in case of an OPTIONS request, it's going to the default handler where you're checking for token and if it's not available, you respond with a 403
. So I suggest you change your code to handle OPTIONS
request as well.