Angular $q.reject().success(), does that make sense? - javascript

I'm reading a book called MEAN Machine, and as I'm getting to the late chapters, I've got stuck at one of the sample applications, that doesn't seem to work.
The problem seems to occur because my mainController calls authService's Auth.getUser() method, which may return either a $http.get() or a $q.reject(). As I'm not logged in, it returns $q.reject(), and fails to chain the .success() promise.
It throws following exception:
TypeError: undefined is not a function at mainCtrl.js:13
My code is as follows.
CONTROLLERmainController
angular.module('mainCtrl', [])
.controller('mainController', function($rootScope, $location, Auth) {
var vm = this;
// check to see if a user is logged in on every request
$rootScope.$on('$routeChangeStart', function () {
vm.loggedIn = Auth.isLoggedIn();
// get user information on route change
Auth.getUser()
/* ========= PROBLEM HERE ========= */
.success(function (data) {
vm.user = data;
});
});
// ... other stuff
});
SERVICEauthService
angular.module('authService', [])
// ===================================================
// auth factory to login and get information
// inject $http for communicating with the API
// inject $q to return promise objects
// inject AuthToken to manage tokens
// ===================================================
.factory('Auth', function ($http, $q, AuthToken) {
var authFactory = {};
// get the user info
/* ========= PROBLEM LEADS HERE ========= */
authFactory.getUser = function () {
if (AuthToken.getToken())
return $http.get('/api/me', { cache: true });
else {
return $q.reject({ message: 'User has no token.' });
}
}
What am I missing?

Replace your call to the service with:
Solution A: .then(successCallback, errorCallback):
Auth.getUser().then(
function (response) { ... }, // success handler
function (response) { // error handler
// case where user is not logged in
// or http request fails
});
or
Solution B: .then(successCallback).catch(errorCallback):
Auth.getUser()
.then(function (response) { ... }) // success handler
.catch(function (response) { // error handler
// case where user is not logged in
// or http request fails
});
Explanation :
Your getUser method is defined as follows :
authFactory.getUser = function () {
if (AuthToken.getToken())
return $http.get('/api/me', { cache: true });
else {
return $q.reject({ message: 'User has no token.' });
}
}
But the success and the error shorthand methods are specific to $http. They doesn't exist in angular's promise $q API. Therefore, when the user is not logged in, because you are returning a $q promise, you got an undefined is not a function.
The methods you can call on a $q promise object are (link to documentation) :
then(successCallback, errorCallback, notifyCallback)
catch(errorCallback) which is a shorthand for promise.then(null, errorCallback)
finally(callback, notifyCallback)

Related

Authentication - $http's .then() success callback called instead of error callback

This may just come from a misunderstanding of how to best do authentication in a MEAN stack app, or my lack of knowledge in how promises and $http's .then() method works, but whenever I try to authenticate to my backend Node server with incorrect credentials, it is calling the success callback of $http's .then() method instead of the error callback. Here's my setup:
I'm using the jsonwebtoken and express-jwt packages, AngularJS interceptors to add the token the to request and check for status 401 responseErrors, a TokenService that sets/removes, etc JWTs, and a UserService to handle login, logout, etc.
From debugging, here's what's happening:
Request to login is sent out
Server catches request, looks for specified user and can't find them in database. Returns 401 error with JSON object including error message, etc.
HttpInterceptor engages the responseError method, correctly sees it's a status 401, removes any possible existing token, redirects to /login screen, and returns $q.reject(response).
UserService.login() correctly uses the error callback and does a return response.
Problem - the success callback in my login.js .login() method runs, instead of the second, error callback one. I have a feeling this has to do with what is discussed in this article about promise chaining, but my expertise has its limit right here and I can't quite understand what I should be doing next to tell the next callback in the chain that the previous one had an error...
Here's my setup:
Express:
authRoutes.js
authRoutes.post("/login", function (req, res) {
User.findOne({username: req.body.username}, function (err, user) {
if (err) res.status(500).send(err);
if (!user) {
res.status(401).send({success: false, message: "User with the provided username was not found"})
} else if (user) {
bcrypt.compare(req.body.password, user.password, function (err, match) {
if (err) throw (err);
if (!match) res.status(401).json({success: false, message: "Incorrect password"});
else {
var token = jwt.sign(user, config.secret, {expiresIn: "24h"});
res.json({token: token, success: true, message: "Here's your token!"})
}
});
}
});
});
From debugging, when I log in with incorrect credentials, it's correctly hitting the res.status(401).send(...) line, so this part seems to be okay.
Angular:
app.js (includes HttpInterceptor)
var app = angular.module("TodoApp", ["ngRoute"]);
app.factory("AuthInterceptor", ["$q", "$location", "TokenService", function ($q, $location, TokenService) {
return {
request: function (config) {
var token = TokenService.getToken();
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = "Bearer " + token
}
return config;
},
responseError: function (response) {
if (response.status === 401) {
TokenService.removeToken();
$location.path("/login");
}
return $q.reject(response);
}
}
}]);
app.config(function ($routeProvider, $httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
$routeProvider
.when("/", {
templateUrl: "landing/landing-page.html"
});
});
userService.js
var app = angular.module("TodoApp");
app.service("UserService", ["$http", "TokenService", function ($http, TokenService) {
this.signup = function (user) {
return $http.post("http://localhost:8080/auth/signup", user).then(function (response) {
return response;
}, function (response) {
return response;
});
};
this.login = function (user) {
return $http.post("http://localhost:8080/auth/login", user).then(function (response) {
if (response.data.success) TokenService.setToken(response.data.token);
return response;
}, function (response) {
return response;
})
};
this.isAdmin = function (user) {
return user.admin;
};
}]);
login.js (Where the problem seems to manifest itself)
var app = angular.module("TodoApp");
app.config(function ($routeProvider) {
$routeProvider
.when("/login", {
templateUrl: "auth/login.html",
controller: "LoginController"
})
});
app.controller("LoginController", ["$scope", "$http", "$location", "UserService", "TokenService", function ($scope, $http, $location, UserService, TokenService) {
$scope.login = function (user) {
UserService.login(user).then(function (response) {
$location.path("/todo");
}, function (response) {
console.log("There was a problem: " + response);
});
}
}]);
That last part, UserService.login(user).then(function (response) { $location.path("/todo"); is the line that is running and trying to redirect the user to the list of Todo items, when I want it to run that console.log("There was a problem: " + response); line instead...
Like I said above, I have a feeling it has to do with chaining promises and how errors are handled partway through the chain instead of bubbling down through the chain. Not sure if I need to add a .catch() block like the site I mentioned above says to do. And even if that is the answer, I'm not entirely sure how to write that.
If there is a better way I should be organizing this, I'm definitely open to suggestions as well. I have to teach this to a class of students and want to make sure I'm teaching good practices.
Thanks in advance for the help!
Take a closer look at this part of your code:
this.login = function (user) {
return $http.post("http://localhost:8080/auth/login", user).then(function (response) {
if (response.data.success) TokenService.setToken(response.data.token);
return response;
}, function (response) {
return response;
})
}
Here you've provided error callback with a return value which is passed to the next callback in promise chain. The source of your confusion is that you still need to return rejected promise of throw from the callback if you want the error to propagate further. Otherwise, it effectively means that you've recovered from error situation and the next step in the flow will be success. This is what you have now.
In your case you either remove error callback altogether
return $http.post("http://localhost:8080/auth/login", user).then(function (response) {
if (response.data.success) TokenService.setToken(response.data.token);
return response;
});
... or make sure you return failed promise
return $http.post("http://localhost:8080/auth/login", user).then(function (response) {
if (response.data.success) TokenService.setToken(response.data.token);
return response;
}, function (response) {
return $q.reject(response);
});
... or throw:
return $http.post("http://localhost:8080/auth/login", user).then(function (response) {
if (response.data.success) TokenService.setToken(response.data.token);
return response;
}, function (response) {
throw new Error(response);
});
Have you tried to use $q.reject in error cases for then() calls?
E.g.
// remember to add $q to deps
this.login = function (user) {
return $http.post("http://localhost:8080/auth/login", user).then(function (response) {
if (response.data.success) TokenService.setToken(response.data.token);
return response;
}, function (response) {
$q.reject(response);
})
};
Related docs: https://docs.angularjs.org/api/ng/service/$q

How to get response from service on controller

I'm trying to separate the $http.post() call into a ".factory()", But would like to fetch the response which is coming async on the controller. Is there a way of doing that?
Controller:
Login.post($scope.user);
Factory:
.factory( 'Login' , function($http,SERVERURL){
var serverUrl = SERVERURL;
return {
'post' : function(user){
$http.post(serverUrl+'/login', user).
then(function(response) {
console.log(response);
}, function(response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
}
};
})
There is a .then() but I want that on the controller, so I can behave accordingly. Thank you!
Basically you need to return the $http.post promise, and from success function you could return a data that will return to the consumer of this method. So that you could easily call the factory method from controller & inside .then function of that call you could have success and error function.
Code
.factory('Login', function($http, SERVERURL) {
var serverUrl = SERVERURL;
return {
'post': function(user) {
return $http.post(serverUrl + '/login', user).
then(function(response) {
console.log(response);
return response.data; //return data from here
}, function(response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
}
};
})
Controller
Login.post().then(function(data){ //success function
console.log(data)
}, function(error){ //error function
console.log(error);
})
You could add a callback param.
.factory( 'Login' , function($http,SERVERURL){
var serverUrl = SERVERURL;
return {
'post' : function(user, callback){
$http.post(serverUrl+'/login', user).
then(function(response) {
console.log(response);
callback(null, response);
}, function(response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
callback(response);
});
}
};
})
And your controller will become:
Login.post($scope.user, function(err, response) {
if(err) {} //do something if there is an error
// or deal with the response
});
To return any response to controller just do:
return {
'post' : function(user){
return $http.post(serverUrl+'/login', user);
}
};
In your controller you will already call.then()
Angular's $http methods return a Promise.
The $http API is based on the deferred/promise APIs exposed by the $q service.
Factory
Your method post is not yet returning anything but can quite simply return the Promise which is created by calling $http.post:
.factory('Login' , function($http, SERVERURL){
var serverUrl = SERVERURL;
return {
'post' : function (user) {
return $http.post(serverUrl + '/login', user)
// ^^^^^^
.then(function (response) {
console.log(response);
return response.data;
}, function (response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
}
};
});
Controller
Then consume the result of the returned Promise by calling then on it:
Login.post($scope.user).then(function (res) {
// do something with `res`...
});

Angular ui-router not injecting the resolve into controller

I'm just getting started with Angular and Express and facing tough times with it. I come from a java background and want to learn Angular and Express and therefore trying out to build one small application.
What I'm trying to do: I have given a password reset link to user so as to change his password. The link is something like:
localhost:9000/reset/:token
Now, I have created a simple view which shows an input box to change his password if token is valid otherwise prints an error if token is invalid based on ng-show property of angular.
Problem: Before I can render my above created view, I want ui-router to check if the :token is valid or not. I will be using the information of validity of token in my controller to control ng-show property mentioned above.
After reading this I tried to leverage the $stateProvider.state functionality with a resolve so as to get the response of validation of token as pre-requisite. This will help me when rendering the actual view where I'm using ng-show technique to show error message or input box to change the password based on the ui-router resolve promiseObject.
What is the issue now ?
Well, after breaking my head for too long, I decided to post my question over here. Can anyone please help me here ?
My questions:
1. I'm able to get the data/err from the api call but somehow ui-router is not injecting it in my controller. Can anyone tell me am I doing anything wrong here ?
2. Right now if the token is not valid, I'm returning a 404 in response from my backend api. But the factory method in frontend takes it as err (Is this expected in Node.js ?) and the err is thrown which results in deferred.reject(). Now, if I go with ui-router definition, if the promise is not resolved then the view won't be rendered, right ? Is there any way by which I can pass this err also to my controller ? Reason why I'm asking to pass err is, my view's ng-show logic is based on the response code (4xx/2xx) depending on which I'll show error message or input box.
Code snippets:
Factory Method which calls the rest api:
isPasswordResetTokenValid: function(token, callback) {
var cb = callback || angular.noop;
var deferred = $q.defer();
return User.getUserByPasswordResetToken(token,
function(data) {
deferred.resolve(data);
return cb(data);
},
function(err) {
deferred.reject(err);
return cb(err);
}.bind(this)).$promise;
}
'use strict';
angular.module('scrubApp')
.config(function ($stateProvider) {
$stateProvider
.state('passwordreset', {
url: '/reset/:token',
templateUrl: 'app/account/passwordreset/passwordreset.html',
resolve: {
promiseObj2: function($stateParams, Auth){
var token = $stateParams.token;
console.log(token);
var response = Auth.isPasswordResetTokenValid({token: token})
.then( function(response) {
console.log(response); // shows response
if(response.status == 404) {
//$scope.token-expiry = true;
return response;
}
if(response.status == 200) {
// $scope.user = response.data;
}
})
.catch( function(err) {
console.log(err); // shows error
return err;
});
}
},
controller: 'ResetPasswordCtrl'
});
});
ResetPasswordCtrl controller:
'use strict';
angular.module('myApp')
.controller('ResetPasswordCtrl', function ($scope, User, Auth, Mailer, $stateParams, promiseObj2) {
$scope.errors = {};
$scope.user = {};
console.log(promiseObj2); // This is coming undefined
$scope.isTokenExpired = promiseObj2; // Not able to inject promiseObj2
$scope.isFormSubmitted = false;
});
Thanks in advance
Your resolve promiseObj2 should return a promise from promise service, so that your controller will wait till promise gets resolved.
return Auth.isPasswordResetTokenValid({token: token})
Update
If you want to handle some logic on failure of your token request then you could handle it in your promise itself, that can do couple of thing like
You could redirected to other page using $state.go('login') or $state.go('error') page.
Code
promiseObj2: function($stateParams, Auth, $state){
var token = $stateParams.token;
console.log(token);
return Auth.isPasswordResetTokenValid({token: token}) //added return here
.then( function(response) {
console.log(response); // shows response
if(response.status == 404) {
$state.go('error')
}
if(response.status == 200) {
return response;
}
})
.catch( function(err) {
console.log(err); // shows error
return err;
});
}
If you want to show html page anyhow if error occurs then You could also return data from the .then of promiseObj2 object that will have information about error message. So that error information is return to the controller
Code
promiseObj2: function($stateParams, Auth, $state){
var token = $stateParams.token;
console.log(token);
return Auth.isPasswordResetTokenValid({token: token}) //added return here
.then( function(response) {
console.log(response); // shows response
if(response.status == 404) {
return {status: 404, data: "doen't found resource"}
}
if(response.status == 200) {
return response;
}
})
.catch( function(err) {
console.log(err); // shows error
return err;
});
}
Then inside controller we will get resolve the promise of promiseObj2 object and then you will get the value of error in the .then function of it.
angular.module('myApp')
.controller('ResetPasswordCtrl', function ($scope, User, Auth, Mailer, $stateParams, promiseObj2) {
$scope.errors = {};
$scope.user = {};
promiseObj2.then(function(resp){
console.log(resp)
$scope.isTokenExpired = resp.isTokenExpired;
}, function(err){
console.log(err)
})
});
Update
If we want to handle a condition where server return 4XX status that means our ajax will call catch function that won't return promise though. We could solve this case by creating custom promise using $q and we will resolve it from the promiseObj2
Code
promiseObj2: function($stateParams, Auth, $state, $q){
var token = $stateParams.token,
deffered = $q.defer();
console.log(token);
Auth.isPasswordResetTokenValid({token: token}) //added return here
.then( function(response) {
console.log(response); // shows response
if(response.status == 404) {
//return {status: 404, data: "doen't found resource"}
deffered.resolve({status: 404, data: "doen't found resource"});
}
if(response.status == 200) {
//return response;
deffered.resolve(response);
}
})
.catch( function(err) {
console.log(err); // shows error
deffered.resolve(err);
});
return deffered.promise;
}

AngularJS : How to properly mock a Promise returned from $http

I have been fighting with this for a little bit now and need some guidance. I would like to unit test this angular service... specifically, the failed part of the promise.
(function () {
angular.module('testable')
.factory('myService', ["$http", "$q", function ($http, $q) {
return {
createThing: function(thing) {
return $http.post("//...", thing)
.then(function (response) {
return response.data;
}, function (response) {
return $q.reject(response.statusText);
});
}
};
}]);
}());
I have been through a number of SO posts about this but each one seems to be a little different. First of all, if any of my service code is not correct in setting up the promise, please stop me there. I have been through about 10 different iterations of testing a rejected promise but nothing is working. Here is what I have:
beforeEach(inject(function ($injector,myService) {
sut = myService;
$httpBackend = $injector.get('$httpBackend');
$q = $injector.get('$q');
$rootScope = $injector.get('$rootScope');
dataToSend = "send me!";
deferred = $q.defer();
}));
it("should get error on error", function () {
var result,
expected = "FAIL!!!!!";
deferred.reject(expected);
$httpBackend.expectPOST(testUrl,dataToSend).respond(deferred.promise);
sut.createThing(dataToSend).then(function (returnFromPromise) {
result = returnFromPromise;
});
$rootScope.$apply();
$httpBackend.flush();
// expect something here but result is always wrong
});
I understand that promises run asynchronously... and I have been cobbling together tidbits of info, but does anyone have advice for completing this unit test?
When you use the $httpBackend service you don't need to work with promises in order to simulate HTTP requests. In fact, the respond() method has the following signature:
respond: function(status, data, headers, statusText) {
...
}
To simulate an error all you have to do is return a non-success status code (!= 200):
it('should not create a thing', function() {
var result;
// Arrange
$httpBackend.expectPOST('https://domain.com/api')
.respond(500, null, null, 'some error');
// Act
myService.createThing('foo').then(angular.noop, function(data) {
result = data;
});
$httpBackend.flush();
// Assert
expect(result).toBe('some error');
});
And here's the whole spec covering both success and error cases:
describe('Testing a factory', function() {
var myService, $httpBackend;
beforeEach(function() {
module('plunker');
inject(function(_$httpBackend_, _myService_) {
$httpBackend = _$httpBackend_;
myService = _myService_;
})
});
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should create a thing', function() {
var result;
// Arrange
$httpBackend.expectPOST('https://domain.com/api')
.respond(200, 'some data');
// Act
myService.createThing('foo').then(function(data) {
result = data;
});
$httpBackend.flush();
// Assert
expect(result).toBe('some data');
});
it('should not create a thing', function() {
var result;
// Arrange
$httpBackend.expectPOST('https://domain.com/api')
.respond(500, null, null, 'some error');
// Act
myService.createThing('foo').then(angular.noop, function(data) {
result = data;
});
$httpBackend.flush();
// Assert
expect(result).toBe('some error');
});
});
Working Plunker
Notice that there's no need to call $rootScope.$apply() here, because no digest cycle is needed to update anything.

Angularjs promise rejection chaining

I need to create chained promises:
var deferred = $q.defer();
$timeout(function() {
deferred.reject({result: 'errror'});
}, 3000);
deferred.promise.then(angular.noop, function errorHandler(result) {
//some actions
return result;
}).then(function successCallback(result) {
console.log('what do I do here?');
return result;
}, function errorCallback(result) {
$scope.result= result;
return result;
});
If I put an errorCallback into the first then, the second then will be resolved and its successCallback will be called . But if I remove errorHandler then second promise will be rejected.
According to Angular JS docs the only way to propagate rejection is to return $q.reject(); and it looks not obvious, especially because I have to inject $q service even if it is not needed;
It can also be done by throwing an exception in errorHandler, but it writes exception trace to console, it is not good.
Is there another option to do this in a clear way? And what is the reason? Why it is done? In which case, the current behavior can be useful?
And what the reason why it is done. In which case, the current behavior can be useful?
It can be useful when in errorHandler you could try to repair error state and resolve promise somehow.
var retriesCount = 0;
function doWork()
{
return $http.post('url')
.then(function(response){
// check success-property of returned data
if(response.data.success)
// just unwrap data from response, may be do some other manipulations
return response.data;
else
// reject with error
return $q.reject('some error occured');
})
.catch(function(reason){
if(retriesCount++ < 3)
// some error, let me try to recover myself once again
return doWork();
else
// mission failed... finally reject
return $q.reject(reason);
});
}
doWork().then(console.log, console.error);
Late to the party, but as I am here;
I prefer to use the $http error for its native error handling, rather than returning a success via a 200 and an error status in the response.
printing 400 or 500 errors in the console is not an issue, if you are debugging you see them if not you don't.
angular.module('workModule', [])
// work provider handles all api calls to get work
.service('workProvider', ['$http', '$q', function($http, $q) {
var endpoint = '/api/v1/work/';
this.Get = function(){
// return the promise, and use 404, 500, etc for errors on the server
return $http.get(endpoint);
};
}])
.controller('workController', ['workProvider', function('workProvider'){
workProvider.Get().then(
function(response){ // success
console.log(response.data);
},
function(response){ // error
console.log(response.data);
}
)
}])

Categories

Resources