I would like my AngularJS app to capture "no internet connection" just like how Chrome captures it. When Chrome captures it, it shows net::ERR_INTERNET_DISCONNECTED on the console.log.
Angular's HTTP interceptor is not able to capture it. The only data I get is
rejection.data.status undefined
rejection.status 0
So far, this has been working great. I noticed that the status is 0 when it can't contact anything. This is inside my http interceptor
responseError: function (rejection) {
if (rejection.status == 0) {
// put whatever behavior you would like to happen
}
// .......
return $q.reject(rejection);
}
A simple script that could help you, you could do it in different ways, even loading an image and call a function when this fails.
function getNetworkStatus(callback, timeout, x){
x = new XMLHttpRequest();
x.timeout = timeout,
x.onreadystatechange = function(){
x.readyState == 4 && callback(x.status == 200)
}, x.onerror = function(e){
callback(!1)
}, x.ontimeout = function(){
callback(!1)
}, (x.open("GET", "http://ip-api.com/json/"), x.send());
}
getNetworkStatus(function(isOnline){
console.info(isOnline ? "ONLINE" : "OFFLINE");
},60000);
UPDATE
We define this interceptor in httpProvider, so return strictly an error when a call does not happen successfully
angular.module('MyApp', []).config([
'$httpProvider',
function($httpProvider) {
$httpProvider.interceptors.push([
'$q',
function($q) {
return {
responseError: function(res){
console.info("Failed to open url: " + res.config.url, res);
//Angular returns "success" by default, but we will call "error" if data were not obtained.
if(res.data == null && res.status === 0 && res.statusText === ""){
return $q.reject(res) //callback error()
}
return res //return default success()
}
};
}
]);
}
]).run(['$http', function($http) { // --TEST--
//$http.get("http://ip-api.com/json/").success(function(){ //page online
$http.get("https://ip-api.com/json/").success(function(){ //try "https" (page offline to test)
console.info("My great page is loaded - We are online :)",arguments)
}).error(function(){
console.info("ERROR: My great online page is not loaded :/ - possibility of connection loss?",arguments)
});
}]);
You can change a trusted URL that you think will never be offline, for example Google, but remember, the url should have origin access permissions that does not show the "Access-Control-Allow-Origin" error. http://ip-api.com/json/ is ok.
Related
Here's my code.
$scope.init = function () {
$scope.urlParam = $location.search();
if ($scope.urlParam.token == undefined || $scope.urlParam.id == undefined) {
$scope.is_start = false;
alert("您没有权限投票");
} else {
$http.get('/vote/validate_querystring?token=' + $scope.urlParam.token + "&id=" + $scope.urlParam.id)
.success(function (response) {
if (response.status == 3) {
$scope.is_start = false;
alert("您没有权限投票");
} else if (response.status == 4) {
$scope.is_start = false;
alert("您已完成投票");
} else {
$http.get('/vote/r_vote_setting')
.success(function (response) {
if (response.status == 1) {
$scope.is_start = false;
alert("投票尚未开始");
} else {
$scope.is_start = true;
$scope.voteData = response.data;
}
});
}
})
}
};
I put this function in ng-init so it will be invoked every time the page is loaded.
As you can see, there are two $http.get in this function. The problem is when I hit the back button to go back to this page, $http.get('/vote/validate_querystring?token=') would be loaded from browser cache while $http.get('/vote/r_vote_setting') makes a new request to the server. I found this from chrome console.
Request URL:http://localhost:8080/vote/validate_querystring?token=202cb962ac59075b964b07152d234b70&id=1
Request Method:GET
Status Code:200 OK (from disk cache)
Remote Address:[::1]:8080
Referrer Policy:no-referrer-when-downgrade
Request URL:http://localhost:8080/vote/r_vote_setting
Request Method:GET
Status Code:200 OK
Remote Address:[::1]:8080
Referrer Policy:no-referrer-when-downgrade
I want to know why this happens and how to make them both send request to the server rather than cache when hitting back button.
You can use cache: false option of $http.
$http.get('/vote/validate_querystring?token=' + $scope.urlParam.token + "&id=" + $scope.urlParam.id,cache: false);
Use $httpProvider to set caching false
myModule.config(['$httpProvider', function($httpProvider) {
//initialize get if not there
if (!$httpProvider.defaults.headers.get) {
$httpProvider.defaults.headers.get = {};
}
// Answer edited to include suggestions from comments
// because previous version of code introduced browser-related errors
//disable IE ajax request caching
$httpProvider.defaults.headers.get['If-Modified-Since'] = 'Mon, 26 Jul 1997 05:00:00 GMT';
// extra
$httpProvider.defaults.headers.get['Cache-Control'] = 'no-cache';
$httpProvider.defaults.headers.get['Pragma'] = 'no-cache';
}]);
You can also include a time parameter to the call, that would every call unique, thus avoid unwanted caching for only that specific call instead having to mess with $http default settings and potentially affect every other calls on the page
$http.get({
url: sampleUrl,
method: 'GET',
params: {
time: new Date()
}
}).error(function (err) {
console.log('Error encountered: ' + err);
}).success(function (data) {
console.log(data);
});
If the user is on a page for a long time and the session ends, if they proceed to make an AJAX call after the session is already expired.. instead of receiving the JSON object, it instead receives the HTML of the login page.
Ideally I'm trying to make it so that it will redirect to a log in page.
Is there any way i can detect this?
I already have an ActionFilterAttribute that works for non-AJAX calls like so:
public class VerifySessionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var userId = filterContext.HttpContext.Session["UserId"];
var userName = filterContext.HttpContext.Session["UserName"];
if (userId == null || userName == null)
{
filterContext.Result = new RedirectResult(string.Format("/Account/Login"));
return;
}
base.OnActionExecuting(filterContext);
}
}
But that doesn't get hit for the scenario above during AJAX calls.
I've also tried an Interceptor.. something like this:
app.factory('httpAuthInterceptor', function ($q) {
return {
'responseError': function (response) {
// NOTE: detect error because of unauthenticated user
if ([401, 403].indexOf(response.status) >= 0) {
// redirecting to login page
//$state.go('home');
$window.location.href = '/Account/Login';
return response;
} else {
return $q.reject(rejection);
}
}
};
})
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('httpAuthInterceptor');
});
But in the same scenario it doesn't seem to hit there as well during the expired session / AJAX call
Is there anything I can do to detect this? When the session is expired I just want to redirect to the login page.. Thanks for any help
EDIT: here's how I make my calls
app.factory('HeadlinesFactory', ['$http', function ($http) {
var HeadlinesFactory = {};
HeadlinesFactory.getShowsForClient = function (clientId) {
return $http({
method: 'POST',
url: '/Show/GetShowsForClient',
data: { clientId: JSON.stringify(clientId) }
});
};
//etc
EDIT2: how all my controllers look like. Except my Account Controller where I put the VerifySession in front of everything except the Login page to prevent loop redirects:
[Authorize]
[CustomFilters.VerifySession]
public class ShowController : Controller
{ ... }
Ajax requests will not process redirect requests for security reasons. In addition, since you are returning a redirect result, a 401/403 status code is not thrown but rather a 302 is returned.
What you could do is expand your filter to conditionalize logic based on whether or not the request is an ajax request. In addition, based on your comments, it seems like creating a new Authorize attribute instead would be the right way to go since that way you can simply replace the default Authorize attribute with your own logic.
public class VerifySessionAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect =
true;
}
else
{
filterContext.Result = new RedirectResult(string.Format("/Account/Login"));
return;
}
}
}
}
This would allow your Angular interceptor to pick up the request and handle it appropriately.
Since IsAjaxRequest looks explicitly for the "X-Requested-With": "XMLHttpRequest" and AngularJS no longer provides that header with Ajax requests, you can add a configuration to the $httpProvider to always include the header.
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
}]);
Please find below the angularjs factory method to call http request:
var HttpService = angular.module("HttpService",[]);
HttpService.factory("HttpServiceFactory",['$http', '$q', '$location', '$rootScope' ,function($http, $q, $location, $rootScope){
return {
getData: function(url, headers, bOnErrorRedirect, bShowInPageError, params){
var headerParam = {'Accept':'application/json'};
if(headers !== undefined || headers !== null){
headerParam = $.extend(headerParam, headers);
}
var updatedParams = {'TimeStamp':new Date().getTime()};
updatedParams = $.extend(params, updatedParams);
var deferred = $q.defer();
$http.get(url,{
headers: headerParam,
params : updatedParams
}).success(function(successResponse){
if(successResponse){
var responseJSON = angular.fromJson(successResponse);
if(responseJSON && responseJSON.messages && responseJSON.messages.length){
//Process Error
}else{
deferred.resolve(successResponse);
}
}else{
deferred.resolve(successResponse);
}
}).error(function(errorResponse , status){
//Process Error
console.error("status here:: "+status);
});
return deferred.promise;
}
}
}]);
And I am calling this method in controller with all required dependencies as below:
HttpServiceFactory.getData(args.sURL,null,false,true,args.oQueryParams).then(function(response){
scope.bDataLoading = false;
// process data
})
.catch(function(oResponse) {
scope.bDataLoading = false;
scope.bDisplayError = true;
// process error
});
Here everything works fine. But the issue is when I've multiple http calls on a page, the UI freezes and does not allow to interact till the request has been processed.
For example, on a page I am displaying 2 angular-ui-grid based on user's selected criteria by input box and calendar control. In such case, the UI freezes until both grids have been displayed or error message has been displayed.
During http service call, user can not do anything but simply wait to finish the request.
How do I resolve the issue of UI freezing ? Is it a true async behavior ? If not, what am I missing to achieve correct async behavior ?
In the extension I am building, on click of a button, a request is sent from ChromeUtils.js
return new Promise(function(fulfill,reject){
var request = {
type : "background.twitterRequestToken",
};
chrome.runtime.sendMessage(request, function(response) {
if (response)
{
fulfill(response);
}
else
{
reject(response);
}
});
});
to the background.js which in turn calls an oauth Api to send a request to the remote server.
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
console.log("background.js: " + JSON.stringify(request));
var type = request.type;
if (type == "background.twitterRequestToken")
{
oauth.authorize().then(function(token,secret,userId,screenname){
console.log("Sending Response on Authorize");
sendResponse({success:true,userId:userId,screenName:screenname});
});
console.log("Before Return true");
return true;
}
I am using the oauth scripts from the example given at https://developer.chrome.com/extensions/tut_oauth but due to a requirement have modified the oauth to use promise. When implemented as a promise, the sendResponse fails but when using the default implementation using callbacks. the sendResponse goes through.
Default implementation using callbacks:
if (type == "background.twitterRequestToken")
{
oauth.authorize(function(token,secret,userId,screenname){
console.log("Sending Response on Authorize");
sendResponse({success:true,userId:userId,screenName:screenname});
});
console.log("Before Return true");
return true;
}
Error message in the background console when implemented as a promise is
Uncaught (in promise) Error: Attempting to use a disconnected port object
The reason I want to use a promise is that when sending signedRequests using Oauth, I want to include part of the xhr response in the sendResponse, e.g. as shown below.
if (type == "background.tweet")
{
var status = request.tweet.text;
var url = "https://api.twitter.com/1.1/statuses/update.json";
var request = {
'method':'POST',
'parameters': {
'status':status
}
}
oauth.sendSignedRequest(url,request).then(signedRequestCallback).then(function(resp,xhr){
console.log("signedRequestCallback success");
console.log("Sending response");
sendResponse({success:true,status_id:resp.id});
});
return true;
}
I have already included return true in my code and as I explained it works fine when not using Promise. It fails when using Promise because the of the error message I included.
===============Update========
To repro this I created a simpler example but I do not face the issue there when I use a Promise. So this may not have anything to do with Promise.
I’m trying the convert an unstructured jQuery application to MVC AngularJS but at one point I’m kind of lost or I just don’t get it… (Unfortunately I’m not a JavaScript-God so the error might also be there)
This is the snippet of the original jQuery code.
$.ajax({
type: "GET",
url: "rest/users/" + userId + "/requests",
accept: "application/json; charset=utf-8",
statusCode: {
200: function(data) {
// do something
},
404: function() {
window.location = 'index.html';
},
500: function() {
alert("Server Error!");
}
}
});
A simple REST call where the HTTP response code is used to navigate. Unfortunately I can’t make this run in AnglularJS.
Here is my Controller:
// Inside the RequestController
$scope.requests = RequestModel.getRequestsByUser($rootScope.currentUser.userId);
if($scope.requests.statusCode == 200) {
// do something
}
else if($scope.requests.statusCode == 404) {
$location.path('/notFound');
} else if ($scope.requests.statusCode == 500) {
$location.path('/error');
}; // PROBLEM: The if/else statement is never true since statusCode is not available
Here is my Model:
// Inside the RequestModel
this.getRequestsByUser = function(userId) {
var RequestResource = $resource('../rest/users/' + userId + "/requests");
var requestList = RequestResource.get({}, function(response, getResponseHeaders) {
// PROBLEM: The property "stausCode" is "unavilable" at the Controller even if it was set here
requestList.statusCode = 200;
console.log("SUCCESS: getRequestsByUser() -> StatusCode: requestList.statusCode");
console.log(requestList);
}, function(response, getResponseHeaders) {
requestList.statusCode = response.status;
console.log("FAILED: getRequestsByUser() -> StatusCode: " + response.status);
});
return requestList;
};
This doesn't work since “statusCode” is “unavailable” inside my controller. The REST call works and also the data binding to the view is fine. I’m just not able to implement the “navigation part”. Do I miss something like $watch properties, asynchronous behavior or is my approach just incorrect?!
Thanks for your help!
You can make better use of resource parameter mapping in your service:
// Inside service
this.requestsByUser = $resource('../rest/users/:userId/requests', {userId:'#userId'});
That way you'll be able to reuse the same resource for different rest actions (eg. post, delete).
And controller code to handle statuses (response handlers were moved to controller):
// Inside the RequestController
$scope.requests = RequestModel.requestsByUser
.get(
{userId: $rootScope.currentUser.userId},
function(response) { // success handler
if(response.status == 200) {
// do something
}
},
function(response) { // error handler
if(response.status == 404) {
$location.path('/notFound');
} else if (response.status == 500) {
$location.path('/error');
}
}
);
Another way around is to use $q service to return promises from your service. But provided solutions seems cleaner to me