I want add a response interceptor to my $http service for error handling purposes. The interceptor logic include send errors messages to server using $http in case necessary, BUT I don't want send errors messages to the server about errors messages, I mean, I want disable my interceptor while sending error message to the server.
My idea was create a service named 'remote_log' and put inside it all the code needed to send error to server. That service of course will use the $http service and have it in its dependency list.
Then add as dependency of the interceptor to the 'remote_log' service, and use the 'remote_log' inside the interceptor when need send errors to the server. The problems is that:
Interceptors must be defined using the $httpProvider when the $http service still is not instantiated/accessible, so, inside the interceptor code can't be a dependency to that the $http service because a "Circular dependency" error happen.
I think my only option is create a separate instance of the $http service inside my 'remote_log', an instance that don't uses the $httpProvider configuration I set while creating the interceptor. My question is: How can I do that? Any other ideas?
1. Circular dependency problem.
So, why does the error appear? Here is a quick overview of the process:
$http service is requested.
$httpProvider is asked to construct it.
During construction you register interceptor, that requests $http service not existing yet.
You get "Circular dependency" error.
First solution.
Create your dependency using angular.injector(). Notice, that you will create another $http service, independent from your app.
$httpProvider.interceptors.push(function($q) {
$injector = angular.injector();
return {
response: function(response) {
$injector.invoke(function($http) {
// This is the exterior $http service!
// This interceptor will not affect it.
});
}
};
});
Second solution (better).
Inject $injector in your interceptor and use it to retrieve dependencies after $http initialization, right at the time you need them. These dependencies are registered services of your app and will not be created anew!
$httpProvider.interceptors.push(function($q, $injector) {
return {
response: function(response) {
$injector.invoke(function($http, someService) {
// $http is already constructed at the time and you may
// use it, just as any other service registered in your
// app module and modules on which app depends on.
});
}
};
});
2. Interception prevention problem.
If you use the second solution, there are actually two problems:
If you utilize $http service inside your
interceptor, you may end up with infinite interceptions: you send
request, interceptor catches it, sends another, catches another,
send again, and so on.
Sometimes you want just prevent request from being intercepted.
The 'config' parameter of $http service is just an object. You may create a convention, providing custom parameters and recognizing them in your interceptors.
For example, let's add "nointercept" property to config and try duplicate every user request. This is a silly application, but useful example to understand the behavior:
$httpProvider.interceptors.push(function($q, $injector) {
return {
response: function(response) {
if (response.config.nointercept) {
return $q.when(response); // let it pass
} else {
var defer = $q.defer();
$injector.invoke(function($http) {
// This modification prevents interception:
response.config.nointercept = true;
// Reuse modified config and send the same request again:
$http(response.config)
.then(function(resp) { defer.resolve(resp); },
function(resp) { defer.reject(resp); });
});
return defer.promise;
}
}
};
});
Having the testing for property in interceptor, you may prevent the interception in controllers and services:
app.controller('myController', function($http) {
// The second parameter is actually 'config', see API docs.
// This query will not be duplicated by the interceptor.
$http.get('/foo/bar', {nointercept: true})
.success(function(data) {
// ...
});
});
I used what is described in the answer but I used the syntax with a factory because with the anonymous function it didn't work, I don't really know why:
(function(angular){
angular.module('app', [])
.config([
'$httpProvider',
function($httpProvider) {
$httpProvider.interceptors.push('Interceptor');
}
])
.factory('Interceptor', [
'$injector',
InterceptorFactory
]);
function InterceptorFactory($injector){
return {
request: function(config) {
var ServiceWithHttp = $injector.get('ServiceWithHttp');
// Use ServiceWithHttp
return config;
}
};
}
}(window.angular));
Related
I don't know how to detect a proxy object change without display it.
I Use async API call to set a proxy object (from a parent component).
In my child component, I use it like this
computed: {
user() { return dataProxy.getCurrentUser() // return a proxy object }
}
updated: {
console.debug(this.user)
if(this.user.id !== undefined) {
loadUserDetails(this.user)
}
}
When the component is mounted, updated method is called and the console display an empty proxy object (ok)
But when the proxy object is updated from api response, the updated method is never called.
Now, If I add {{user}} on my template, the updated method is called after the API response with the right content.
how to detect a proxy object change without display it ?
You should use a watch function:
watch: {
user(newUser) {
console.log('user changed:', newUser);
}
}
I used service worker to cache the resource from the other domain. I get this error "TypeError: Request failed serivce-worker.js:12" I don't know why this error is occurring.
service-worker.js
var cacheNames=['v1'];
var urlsToPrefetch=['file from other domain'];
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(cacheNames).then(function(cache) {
console.log('Service Worker: Caching Files');
cache.addAll(urlsToPrefetch.map(function (urlToPrefetch) {
console.log(urlToPrefetch);
return new Request(urlToPrefetch, {mode: 'no-cors'});
})).catch(function(error){
console.log(error);
});
})
);
});
self.addEventListener('fetch', function(event) {
console.log('Service Worker: Fetching');
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
This is a side-effect of dealing with opaque responses (those fetched with mode: 'no-cors'). Here's an excerpt from this longer answer:
One "gotcha" that developer might run into with opaque responses involves using them with the Cache Storage API. Two pieces of background information are relevant:
The status property of an opaque response is always set to 0, regardless of whether the original request succeeded or failed.
The Cache Storage API's add()/addAll() methods will both reject if the responses resulting from any of the requests have a status code that isn't in the 2XX range.
From those two points, it follows that if the request performed as part of the add()/addAll() call results in an opaque response, it will fail to be added to the cache.
You can work around this by explicitly performing a fetch() and then calling the put() method with the opaque response. By doing so, you're effectively opting-in to the risk that the response you're caching might have been an error returned by your server.
const request = new Request('https://third-party-no-cors.com/', {mode: 'no-cors'});
// Assume `cache` is an open instance of the Cache class.
fetch(request).then(response => cache.put(request, response));
I'm attempting to create a program where I use the Steam API. I want to be able to call the method to retrieve a user's info from the client, while keeping the actual code of the method secret from the client, since it contains an API Key. I tried defining the methods as global in a server folder, like this:
key = 'xxxxxxxxxxxxxxxx';
Meteor.steamFunctions = {
getName: function(user){
var userSteamId = user.profile.id;
Meteor.http.get('http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=' + key + '&steamids=' + userSteamId, function(error, resultJSON){
if (error){
return 'Error in Steam API';
} else {
var json = JSON.parse(resultJSON);
return json.personaname;
}
})
},
getPic: function(user){
var userSteamId = user.profile.id;
Meteor.http.get('http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=' + key + '&steamids=' + userSteamId, function(error, resultJSON){
if (error){
return 'Error in Steam API';
} else {
var json = JSON.parse(resultJSON);
return json.avatarfull;
}
})
}
}
I then try to call it like this in a client-side script:
if (Meteor.isClient){
Template.profile.helpers({
'getName': function(){
return Meteor.steamFunctions.getName(Meteor.user());
}
});
}
That, however, throws
Exception in template helper: TypeError: Cannot read property 'getName' of undefined
at Object.Template.profile.helpers.getName
How can I go about keeping the key secret to the user while still accessing the data?
Well, it is not quite as simple as adding a property to the Meteor global. Also, the remote method/call API to do this will involve asynchronous code.
Put the call to the API, with the secret API key, on the server side in code only visible on the server, e.g. the ./server subdirectory. Define a Meteor.method on the server side that can be called with Meteor.call on the client side.
In the server side Meteor method there are method security checks you can make to check for a logged in user or userid, and use this to decide whether to make the calls or ignore the request. You can throw a new Meteor.Error from the server side if a request is improper or there is an error, but these take resources to communicate.
The thing to understand about Meteor is that it has nothing magical to change how Javascript behaves on the browser or the server. The server is ultimately running nodejs. Objects defined on the server do not magically migrate to the client, or vice versa. If an object is defined on both, it is actually two separate pieces of code.
Therefore, in the client code, the Meteor.call to call the server-side code from the browser... is actually using an existing websocket or ajax API that is asynchronous in nature. This means that you will need to structure client code to provide callback functions on the browser to handle the asynchronously returned results of looking up Name or Pic. A direct return and imperative coding style is not possible.
Typically you'll want to update something on a user's screen as a result of information returned from a lookup. The usual Meteor coding is to have the callback function update a session global variable with Session.set(). Templates can reference these session variables, and through an implied or explicit Tracker.autorun(), the screen can be updated when the API returns the data.
You need to:
Move your steamFunctions into methods which are defined only on the server.
Properly invoke the methods from the client.
Below is some example code based on your original question. Please note this has not been tested and may require some tweaking.
server/methods.js
const KEY = 'xxxxxxxxxxxxxxxx';
const URL = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002';
Meteor.methods({
getName() {
const userSteamId = Meteor.user().profile.id;
const params = {
key: KEY,
steamids: userSteamId,
};
try {
var result = HTTP.get(URL, { params });
// Double check this - I have no idea what this API returns. The value
// you want may be nested under result, like result.data or something.
return JSON.parse(result).personaname;
} catch (e) {
// Something bad happened - maybe throw an error.
return false;
}
},
});
Note this method is defined on the server, so we don't expose our KEY to the client. Also note we are using the synchronous version of the HTTP api, so the value can be returned to the client.
client/lib/user.js
Tracker.autorun(function () {
user = Meteor.user();
if (user && user.profile && user.profile.id) {
Meteor.call('getName', (err, name) => {
Session.set('steamName', name);
});
} else {
Session.set('steamName', '');
}
});
When the user logs is or is updated, get the steam name and set a global session variable.
client/templates/profile.js
Template.profile.helpers({
getName: function () {
return Session.get('steamName');
},
});
Read the steamName session variable for use in your template.
I would like to refresh the state of the user that is used in the navbar. but now I have to call .all(function() { refresh() }); on all $http.post premises.
Could I capture all of them with a configuration for instance?
Yes, you can intercept $http using request/response interceptors.
But a better design might be to set a $watch in the controller for the navigation bar on a property the POST calls change (possibly on $rootScope, though it is not recommended).
This is what I ended up doing:
app.config(function($httpProvider, $provide) {
// This sucks but Angular is so compliated that we can't get the rootScope
// in a simple way inside a factory for a configuration
function getRootScope() {
return angular.element('body').scope();
}
// Call refresh() after each HTTP destructive action
$provide.factory('refreshAfterRequests', function($q) {
return {
'response': function(response) {
// Every non-GET request can potentialy change the state of the user
if (response.config.method !== 'GET') {
getRootScope().refresh();
}
return response;
}
};
});
// The refresh is an interceptor on all HTTP requests made with Angular
$httpProvider.interceptors.push('refreshAfterRequests');
});
If someone have something better like how to get the scope from the inside the interceptor, I will gladly change this answer.
I am simply trying to load data when my app starts. However, the view loads faster than the http request(of course). I want to refresh my view once my data has been properly loaded because that data defines my view.
I've tried $rootScope.apply from inside the factory where I do my http request, and I also tried directly doing the http request in my controller again with $scope.apply, and neither one worked as they both gave me "$digest already in progress"
Any idea how can I set up my code to make my views refresh on data load? I will be having several different http requests and I would like to know how to set them up properly! I would really appreciate any input!
Here is some of the code I am working with.
app.factory('HttpRequestFactory', function($http, $q) {
var HttpRequestFactory = {
async: function(url, params) {
var deferred = $q.defer();
$http({
url: url,
method: post,
params: params
})
.success(function(data, status, headers, config) {
deferred.resolve(data);
})
.error(function(data, status, headers, config) {
deferred.reject("An error occurred");
});
return deferred.promise;
}
};
return HttpRequestFactory;
});
Factory
function initializeAll(){
HttpRequestFactory.async('../api', {action: 'getall'}).then(function(data) {
//$rootScope.$apply(function () {
allData = data;
//});
angular.forEach(allData, function(value, index){
console.log('Voala!');
});
});
}
Controller calling the factory's function initializeAll()
app.controller("MainController", ["$scope", "$rootScope","MyFactory",
function($scope, $rootScope, MyFactory){
MyFactory.initializeAll();
}
]);
Oh my !
You got the f** matter with AngularJS !
In fact you have to do a "safeApply" like that for example :
$rootScope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
In AngularJS you can only have one $apply or $digest loop at the same time.
For details on these loops look at the docs :
http://docs.angularjs.org/guide/concepts
It will explain what is the $apply loop and you'll understand a lot of things about the two-way-data-binding in AngularJS
Hope it helps.
Don't use $apply: use $watch.
Calling $apply is (almost) always the wrong thing to do. The only time you should ever be calling it is if you've triggered a change outside of an 'angular' method; here, since the trigger occurs in an angular $http request, you can't call $apply because it's already being done at that moment by the $http block. Instead, what you want to do is $watch.
Official Doc for $scope.$watch() here
This will let you watch an object and update whenever it changes. I assume that your view is based on allData and you want it to update immediately; if you're using an ng method, then the watch is automatically setup for you and no more work should be needed. If you're using allData yourself inside a controller, you can write the watch in that controller like this:
$scope.$watch(function thingYouWantToWatch(){
return <accessor call to allData here>;
},
function whatToDoOnChange(newValue, oldValue){
$scope.myCoolThing = newValue; //this is the newValue of allData, or whatever you're watching.
}
);