I am trying to improve an AngularJS service for invoking remote JSON-RPC services. In accordance with the JSON-RPC specification, when an exception occurs at the server side, the response should include an error object with the description of the same:
response = {
jsonrpc: "2.0",
result: null,
error: "Description of the error",
id: 1,
}
... where "id" is the identifier of the request original sent to the server.
The Angular component that I am trying to improve (https://github.com/ajsd/angular-jsonrpc) processes the responses from the server using $http transformers. I modified the original transformer, so that the new one looks like this:
transforms.push(function(data) {
//Original code: return data.id === id ? data.result || data.error : null;
if (data.error !== null) {
throw data.error;
}
if (data.id !== id) {
throw '[jsonrpc] Wrong response ID, id = ' + data.id;
}
return data.result;
});
As you can see, I throw an exception with the description of the error; which is a really poor solution since this service is based on $http promises and, therefore, the invoker of the service will find it difficult to catch the exception.
How can I invoke from within the $http transformer the "error" promise that the user originally sets while invoking the $http service?
$http.get(url).then(success(data) {...}, error(data) {...}};
Is this a correct approach or should I better base my modification on interceptors?
if you left the code as you had it originally, you can define a service which uses the jsonrpc module:
angular.module('myApp').
service('jsonrpcservice', function(jsonrpc) {
var service = jsonrpc.newService('svc');
this.get = service.createMethod('getData');
});
..in your controller somewhere:
jsonrpcservice.get({params}).success(function(result){}).error(function(err){});
you can handle the error in the .error() returned by $http
Related
I'm creating an app with SignalR Core on Azure. Now I need to match the invoked request with the returned value and callback. I would like to use the invocationId to make sure the method that will be executed is the latest invoked request. To avoid as happens in the attached image.
var _this.latestInvokeId = ??
connection.invoke('getData', _this.ID)
.then(function (data) {
if(this.invocationId === _this.latestInvokeId)
console.log('Use the data', data)
})
.catch(function (error) {
console.log(error);
});
When the getData is invoked multiple times in a short interval I would like to be sure to use the right response. So the response with invocationId 8 needs to be ignored and only the response with invocationId 9 is used.
I've implemented a new id that is send on the request and can be validated when the value is returned.
I have a shared service throughout my app that handles authentication, I'm trying to abstract my component logic as far from the http requests as possible, however, every bit of documentation I've seen appears to want me to return the httpClient Observable, and subscribe to it and perform all logic for handling the results and errors inside the component.
My current service code is completely broken, but it looks like this:
userLogin(email: String, password: String) {
const body = { email: email, password: password };
return this.httpClient.post('http://localhost/users/login', body).subscribe(
this.handleResults,
this.handleErrors
);
}
handleResults(results: any) {
if (results.errors) {
console.log(results.errors);
} else {
return results;
}
}
handleErrors(err: HttpErrorResponse) {
if (err.error instanceof Error) {
// A client-side or network error occurred. Handle it accordingly.
console.log('A ', err.status, ' error occurred:', err.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.log('Could not connect to server.');
console.log('Backend returned code ' + err.status + ', body was: ' + JSON.stringify(err.error));
}
}
And ideally on the component module I'd have something like a callback function that is called when the response is ready. It seems like I'm trying to go against the grain with Angular patterns, but at the same time, I can't understand why Angular would require error handling for generic http errors do be performed at the Component level.
So I suppose the basic question is: how do I handle httpClient errors within the service, rather than in the individual components?
You can handle the errors in here, and still return the observable, like so
return this.httpClient.post('http://localhost/users/login', body).catch(this.handleErrors)
Imagine the following syntax error in JSON (, instead of :):
[
{
"name": "anna",
"email": "anna#gmail.com",
"town", "london"
},
...
]
I am wondering if it is possible to handle this error instead of getting an exception, getting the erroneous object, correct the error and go on with the correct version.
Here is a part of my Angular service; I am trying to get text and not JSON data but it does not work...
angular.module('mine', [])
.config(function($sceProvider) {
// Completely disable SCE.
$sceProvider.enabled(false);
})
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
'self',
'http://www.mocky.io/v2/5807df4a10000004122b74e2'
]);
}])
.config(function ($httpProvider) {
$httpProvider.interceptors.push(function($q) {
return {
'request': function(config) {
config.headers.Accept = 'text/plain';
return config;
},
'response': function(response) {
try {
// try to parse it
response.data = JSON.parse(response.data);
} catch (ex) {
// try to fix it
console.log("error " + ex);
console.log(response.data);
response.data = {fixed_data : "data"};
}
// return the corect data.
// note that the original response.data WILL BE CHANGED and this is expected.
return response;
}
};
});
})
angular.module('mine').factory('MyFactory', ['$http','$q', function MyFactory($http,$q) {
return {
getData: function() {
var deferred = $q.defer(),
config = {
params: { }
},
url="http://www.mocky.io/v2/5807df4a10000004122b74e2";
$http.jsonp(url,config)
.then(
function (response) {
deferred.resolve(response.data);
},
function (error) {
console.log(error);
return $q.reject('Error retrieving data');
}
);
return deferred.promise;
}
};
}]);
Is there anyway of directing the above promise into the success callback, retrieving the erroneous JSON and correcting it? How may I code that according to the above example?
Or maybe something easier, how to retrieve text and not JSON data from $http.jsonp as not to be driven to the failure callback?
TL;DR
Edits after further understanding the OP problem:
In the generic case where you want to edit content of a response you can do it with "Interceptors" yet the response should be legitimate to begin with. That is, if you wanted to change numeric strings to integers in an otherwise correct JSON - it would be possible.
In the situation the OP is heaving where the JSON is malformed - it is just not possible!
The long story
First
Getting into a classic XY problem!
You should really ask yourself why is the JSON broken and not attempt to fix it in the client code.
Think of it - You will only get into more problems if you fix it now, and later someone will fix the API - then you will have the broken code.
What if the JSON should have been:
[
{
"name": "anna",
"email": "anna#gmail.com",
"addresses": [{"town": "london", ...}, ...]
},
...
]
Or (god forbid):
[
{
"name": "anna",
"email": ["anna#gmail.com","town", "london"]
},
...
]
You see - my point is - The API is broken, it can be anything. You should fix the API. And if this API is not yours to fix -> use some other API or contact the owner to fix it.
JSONP
JSONP is a way to let APIs to call directly into your code.
You must trust this API. If an API would have giving me malformed JSONs - I would stay away!
In short, the way JSONP works in Angular (or everywhere actually) is by injecting a <script> tag into the DOM with src pointing to the URL of the JSONp request.
The server will pad the JSON data with a function name (most often callback but it can be any globally accessible function (Angular is using angular.callbacks._xyz) and will send it.
The browser then invokes the script that was downloaded from the src.
Now, the thing is that it is the browser calling the script. It is not in the hands of Angular. And that is exactly the problem the OP is confronting - the script must be evaluated as a correct JavaScript to begin with and the browser is doing that, not Angular. It is a MUST. You cannot get in the middle of it. It could pose a security risk if you do. This is why, for instance, the response of a JSONP request will always (by convention...) be returned with MIME type of application/javascript no matter what you ask for.
Warring - here be dragons!
I urge you not to go in this path!
If you are insisting in getting from a JSONP call a JSON with errors (and by errors I mean that the JSON can be parsed as object yet there are some thing you want to change in that object) you could try to add "Interceptors"
.config(function ($httpProvider) {
$httpProvider.interceptors.push(function($q) {
return {
'request': function(config) {
// here you can edit the request.
return config;
},
'response': function(response) {
// response.data will hold your bad data
// you could edit it
response.data = fix(response.data);
// return the correct data.
return response;
}
};
});
})
Note that you could also Overriding the Default Transformations
Also, make sure to also to:
// Whitelist the JSONP endpoint that we are using to show that we trust it
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
'self',
'https://your.api.url/**'
]);
}])
And if all went well you will be able to call:
//uncomment {jsonpCallbackParam: 'callback'} if your jsonp callback
//parameter at the backend uses some other name but the default 'callback'
$http.jsonp(https://your.api.url/*,{jsonpCallbackParam: 'callback'}*/)
.then(function(response) {
$scope.status = response.status;
$scope.data = response.data;
}, function(response) {
$scope.data = response.data || 'Request failed';
$scope.status = response.status;
});
You can use douglascrockford's JSON-js.If it is not a valid json it will throw an error so that you can catch using try/catch and return a new Promise or simple true/false. If you don't use the library it will fallback to built in parser.
$http({
method: "GET",
url: '../data/data-feed.json'
})
.then(
function (response) {
console.log(response);
try {
JSON.parse(json);
console.log("valid");
} catch (e) {
console.log("invalid");
// correct the invalid json here
}
},
function (error) {
console.log('error');
}
);
Default JSON parser behavior
function parseJSON (jsonString){
try {
var jString = JSON.parse(jsonString);
if (jString && typeof jString === "object") {
return jString;
}
}
catch (e) { }
return false;
};
var inValidJson = '[{"name": "anna","email": "anna#gmail.com","town", "london"}]';
var validJson = '[{"name": "anna","email": "anna#gmail.com","town": "london"}]';
console.log("if invalid returns: ", parseJSON(inValidJson));
console.log("if valid get original object: ",parseJSON(validJson));
Short answer: no there isn't.
Long answer: If your JSON serialisation does not work in the backend, you have basically to parse a string an construct a new Object by yourself. There is no library which does that for you. And think of a different backend service.
I agree to #Florian's answer in general - there is no simple way of doing so.
I think that You should:
try to find a clever way to find the problem place;
replace the comma(s);
parse to JSON anew.
Idea of finding problem place: After every second (even) value there needs to be a comma. After every second (odd one - 1., 3., 5) - a colon. On each New { You have to start a new count. It probably is a pain in the ass, but doable.
It's possible, but cumbersome. For reasons It-Z went into, generally the best practice would be to fix the JSON being served by the API or to find a new API to work with. Assuming you have your reasons for not doing that, here is the logic flow:
catch the error from JSON.parse and feed it to a new function, along with the unparsed response string
in that new function
if you know you're only going to have this one case for your error, create logic to find and fix it; regex will be your friend here
if you're trying to catch multiple types of syntax problems, you'll need more complex regex and much more complex logic
pass the corrected string back to the original JSON.parse function
Using Angular 1.5.5 here:
Is there any way to tell Angular to ignore response body for particular requests (such as $save)? It drives me crazy that after I call $save, angular updates the model with the object returned by a server, which initially was supposed to be used to distinguish between different resolutions of the request. It results in unwanted form clear. Interestingly enough, this behaviour remains even if I send a 400 or 500 http status code.
In case you need more info, relevant code is below.
Controller:
'use strict';
angular
.module('app.operators')
.controller('OperatorNewController', OperatorNewController);
OperatorNewController.$inject = ['operatorsService', 'notify'];
function OperatorNewController(operatorsService, notify) {
var vm = this;
vm.done = done;
activate();
function activate() {
vm.operator = new operatorsService();
}
function done(form) {
if (form.$invalid) {
// do stuff
return false;
}
vm.operator.$save(function(response) {
if (response.success && response._id) {
$state.go('app.operators.details', {id: response._id}, { reload: true });
} else if (response.inactive) {
// do stuff
} else {
// do other stuff
}
}, function (error) {
// do other stuff
});
}
}
Service:
'use strict';
angular
.module('app.operators')
.service('operatorsService', operatorsService);
operatorsService.$inject = ['$resource'];
function operatorsService($resource) {
return $resource('/operators/:id/', {id: '#_id'}, {
'update': { method: 'PUT' }
});
}
Server request handler is also fairly simple:
.post('/', function (req, res) {
if (!req.operator.active) {
return res.status(500).json({ inactive: true, success: false });
}
// do stuff
return res.json({ success: true });
});
In either way I don't like the idea of having to send the entire object from server (particularily when it's a failed request), and even if I have to, I still need a way to send some extra data that will be ignored by Angular.
Your help is very much appreciated!
The $save method of the resource object empties and replaces the object with the results of the XHR POST results. To avoid this, use the .save method of the operatorsService:
//vm.operator.$save(function(response) {
vm.newOperator = operatorsService.save(vm.operator, function(response),
if (response.success && response._id) {
$state.go('app.operators.details', {id: response._id}, { reload: true });
} else if (response.inactive) {
// do stuff
} else {
// do other stuff
}
}, function (error) {
// do other stuff
});
UPDATE
It results in unwanted form clear. Interestingly enough, this behaviour remains even if I send a 400 or 500 http status code.
This behavior is NOT VERIFIED.
I created a PLNKR to attempt to verify this behavior and found that the $save method does not replace the resource object if the server returns a status of 400 or 500. However it does empty and replace the resource object if the XHR status code is 200 (OK).
The DEMO on PLNKR
It drives me crazy that after I call $save, angular updates the model with the object returned by a server
It helps to understand how browsers handle traditional submits from forms.
The default operation for a submit button uses method=get. The browser appends the form inputs to the URL as query parameters and executes an HTTP GET operation with that URL. The browser then clears the window or frame and loads the results from the server.
The default operation for method=post is to serializes the inputs and place them in the body of an HTTP POST. The browser then clears the window or frame and loads the results from the server.
In AngularJS the form directive cancels the browser default operation and executes the Angular Expression set by either the ng-submit or ng-click directive. All $resource instance methods including $get and $save, empty and replace the resource object with XHR results from the server if the XHR is successful. This is consistent with the way browsers traditionally handle forms.
In RESTful APIs, HTTP GET operations return the state of a server resource without changing it. HTTP POST operations add a new resource state to the server. APIs usually return the new resource state, with additional information such as ID, Location, timestamps, etc. Some RESTful APIs return a redirect (status 302 or 303) in which case browsers transparently do an HTTP GET using the new location. (This helps to Solve the Double Submission Problem.)
When designing RESTful APIs, it is important to understand how traditional browsers behave and the expectations of RESTful clients such as AngularJS ngResource.
We are developing a Single Page Application with AngularJS and ASP.NET MVC Json Rest API.
When an unauthenticated client tries to navigate to a private route (Ex: /Foo/Home/Template) to get a template, it gets a 401 response from the Web API and our AngularJS app automatically redirects it to the login page.
We are handling the 401 with $http interceptor with something like this:
if (response.status === 401) {
$location.path(routeToLogin);
return $q.reject(response);
}
Entering the correct credentials allows the client to get the template.
Everything is working perfectly except for one detail; the Javascript console reports this error:
Error: [$compile:tpload] http://errors.angularjs.org/1.3.0/$compile/tpload?p0=%Foo%2FHome%2FTemplate%2F
AngularJs documentation states this:
Description
This error occurs when $compile attempts to fetch a template from some
URL, and the request fails.
In our AngularJs app the request fails but it is by design because the resource is there but it cannot be accessed (401).
Should I move on and accept this kind of error on console or is it possible to mute or shield it in some way?
EDIT:
I have debugged the angular source a little bit and I found what part of the code is raising the exception.
Since we are using TemplateUrl to declare our templates, we are indirectly using the function compileTemplateUrl that makes this call:
$templateRequest($sce.getTrustedResourceUrl(templateUrl))
this leaves the second parameter (ignoreRequestError) of templateRequest undefined.
ignoreRequestError(optional)boolean
Whether or not to ignore the exception when the request fails or the
template is empty
When our http interceptor, handling the 401 status code, rejects the promise, the $http.get inside the $TemplateRequestProvider fails and calls this function:
function handleError() {
self.totalPendingRequests--;
if (!ignoreRequestError) {
throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl);
}
return $q.reject();
}
I believe we can't do anything to prevent the error on console as TemplateUrl does not allow to set the ignoreRequestError flag to false.
I've tried to bypass the reject in case of 401 status code; this fixes the error on console but sadly it has a side effect: an empty template is wrongly cached into the TemplateCache causing othe problems.
After some thinking I remembered about decorating in Angular, it solved this problem perfectly:
app.config(['$provide', function($provide) {
$provide.decorator('$templateRequest', ['$delegate', function($delegate) {
var fn = $delegate;
$delegate = function(tpl) {
for (var key in fn) {
$delegate[key] = fn[key];
}
return fn.apply(this, [tpl, true]);
};
return $delegate;
}]);
}]);
You should be able to intercept the call for the template by status and url.
Plunker
app.config(function($httpProvider) {
var interceptor = function($location, $log, $q) {
function success(response) {
// The response if complete
$log.info(response);
return response;
}
function error(response) {
// The request if errors
$log.error(response);
return $q.reject(response);
}
return function(promise) {
return promise.then(success, error);
}
}
$httpProvider.responseInterceptors.push(interceptor);
});
As I see it, you have two options:
Option A)
go with the interceptors. However, to eliminate the compile you need to return success status code inside response error (BAD) OR redirect to the login page inside the interceptor (Good):
app.factory('authInterceptorService', function () {
var interceptor = {};
interceptor.responseError = function (rejection) {
if (rejection.status === 401 && rejection.config.url === "home template url") {
//BAD IDEA
//console.log("faking home template");
//rejection.status = 200;
//rejection.data = "<h1>should log in to the application first</h1>";
//GOOD IDEA
window.location = "/login.html";
}
return rejection;
}
return interceptor;
});
and on app config:
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptorService');
}
Option b)
make the home template public. After all it should be just html mark-up, without any sensible information.
this solution is clean...and perhaps is also possible.