What I want: an error handling that handles different http errors (401, 404, 500) globally. It shouldn't matter where or when an http error occurs.
So far I implemented an error action in the application route that will be called on any adapter errors on route's model hook. That's working fine.
What is not covered is the case when I work with records in other contexts like record.save(). There I need to separately handle the error on the promise.
Moreover I don't only want to have a default error handler but more like a fallback.
Ok, before talking too much let's have an example implementation of my use cases.
application route
The application error action should be the default / fallback error handler.
actions: {
error: function(error) {
var couldHandleError = false;
if (error.errors) {
switch (error.errors[0].status) {
case '401':
// User couldn't get authenticated.
// Redirect handling to login.
couldHandleError = true;
break;
case '404':
case '500':
// Something went unexpectedly wrong.
// Let's show the user a message
couldHandleError = true;
break;
}
}
// return true if none of the status code was matching
return !couldHandleError;
}
}
Some route
In this case the application error action is called.
model: function() {
return this.store.findAll('post');
}
Some controller
In this case the application error action is NOT called.
(I know, the following code probably doesn't make sense, but it is just supposed to illustrate my requirements)
this.store.findRecord('post', 123);
Some other controller
In this example the application error action is not called, for sure, since I use my own handler here (catch()).
But as you can see in the comments I do want to use the default handler for all status codes other than 404.
this.store.findRecord('post', 123).catch(function(reason) {
if (reason.errors[0].status === '404') {
// Do some specific error handling for 404
} else {
// At this point I want to call the default error handler
}
});
So is there a clean and approved way of achieving that? I hope I could make my problem clear to you.
I think I have my final solution I want to share with you. Basically I took the ideas of the guys commenting my question and extended them so they fit my needs.
First I created a mixin with the main logic. Since I want it to be as generic as possible, it distinguishes between a) controller / route and b) jquery / adapter error. So it doesn't matter from where you call it and whether your error object is originally from an jquery Ajax request or an ember adapter.
import Ember from 'ember';
export default Ember.Mixin.create({
ajaxError: function(error) {
if (!error) {
Ember.Logger.warn('No (valid) error object provided! ajaxError function must be called with the error object as its argument.');
return;
}
// Depending whether the mixin is used in controller or route
// we need to use different methods.
var transitionFunc = this.transitionToRoute || this.transitionTo,
couldHandleError = false;
switch (this._getStatusCode(error)) {
case 401:
transitionFunc.call(this, 'auth.logout');
couldHandleError = true;
break;
case 404:
case 500:
// Here we trigger a service to show an server error message.
// This is just an example and currently not the final implementation.
// this.get('notificationService').show();
couldHandleError = true;
break;
}
// For all other errors just log them.
if (!couldHandleError) {
Ember.Logger.error(error);
}
},
_getStatusCode: function(error) {
// First check for jQuery error object
var status = error.status;
// Check for ember adapter error object if it's not a jquery error
if (!status && error.errors && error.errors[0].status) {
status = parseInt(error.errors[0].status);
}
return status;
},
});
Next I reopened some Classes (inside app.js) to make this functionality globally available:
import AjaxErrorMixin from 'app/mixins/ajax-error';
Ember.Route.reopen(AjaxErrorMixin);
Ember.Controller.reopen(AjaxErrorMixin);
Ember.Component.reopen({
_actions: {
// Passing ajaxError per default
ajaxError: function(error) {
this.sendAction('ajaxError', error);
}
}
});
Finally I added some actions to the application route:
actions: {
error: function(error) {
this.send('ajaxError', error);
},
ajaxError: function(error) {
this.ajaxError(error);
},
}
Why do I have two actions doing the same stuff? Well, the error action is called on errors on route's model hook. I could stay with that action, but in the rest of the application where I explicitly call this action I want a more meaningful name. Therefore I also created a ajaxError action. You could stay with one action, for sure.
Now you can use this everywhere:
Route / Controller:
this.ajaxError(error);
Component:
this.sendAction('ajaxError', error);
For sure, you also need to pass the action out of the component to be handled by the application route:
{{some-component ajaxError="ajaxError"}}
This works for nested components, too. You don't need to explicitly send this action further inside the component.js file since we reopened the Component and passt this action into.
I hope I can help other people with that implementation. Also any feedback is welcome.
You can try to do something with these events (put these lines in app.js before app initialization):
Ember.onerror = function (error) {
console.log('Ember.onerror handler', error.message);
};
Ember.RSVP.on('error', function (error) {
console.log('Ember.RSVP error handler', error);
});
Ember.Logger.error = function (message, cause, stack) {
console.log('Ember.Logger.error handler', message, cause, stack);
};
I learned about them from https://raygun.io/blog/2015/01/javascript-error-handling-ember-js/, you may find some details there.
Related
I've been trying to get a User Feedback dialog to show when I click on a certain button, but I've had some trouble. I successfully got it to work when I make a call my API and end up getting an error shown first.
However I created a button that would trigger a call to Sentry.showReportDialog, but I get a 'Cannot read property 'showReportDialog' of undefined' error. I've tried using Sentry.capture Message/Exception/Error to generate an eventId, but I still got the same error. This is my current code that's failing, but I've modified it a decent amount and was still getting the same undefined error for showReportDialog, even when I tried the method that worked with my API call. This web application is running using Ember.js v3.5.1 and in my package.json the dependency for sentry is
"#sentry/browser": "^4.5.3"
// works
try {
$('.ember-application').addClass('request-loading');
this.model.setProperties(properties);
return yield this.model.save();
} catch (err) {
// Get feedback from user through sentry
Sentry.init({
dsn:'https://ec08003a76fa4b6e8f111237ed3ed8e1#sentry.io/1369772',
beforeSend(event) {
if (event.exception) {
Sentry.showReportDialog({ eventId: event.event_id });
}
return event;
},
});
}
// does not work
try {
throw new Error();
} catch (e) {
var eventId = yield Sentry.captureException(e, function(sendErr, eventId) {
// This callback fires once the report has been sent to Sentry
if (sendErr) {
console.error('Failed to send captured exception to Sentry');
} else {
console.log('Captured exception and send to Sentry successfully');
console.log(eventId);
}
});
console.log(eventId);
Sentry.showReportDialog({ eventId: eventId });
}
The following code ended up working for me
try {
throw new Error();
} catch (e) {
Sentry.init({
dsn: 'https://ec080033425613e7ed3ed8e1#sentry.io/1369772',
beforeSend(event) {
return event;
},
});
var eventId = yield Sentry.captureException(e, function() {});
Sentry.showReportDialog({
eventId: eventId,
});
}
Answer for 2022
If anyone stumbles on this question looking for the answer, please don't use the other answer with yield and everything else. You don't need to generate and catch a fake error just to submit a report to Sentry, but you do need an eventId. You can get that from the return value of Sentry.captureMessage(...) like this:
const eventId = Sentry.captureMessage(`User has some feedback`);
Sentry.showReportDialog({
eventId,
title: 'Want to share some feedback?',
subtitle: 'Great!',
subtitle2: `If not, just click 'close' below.`,
labelComments: 'What would you like us to know?',
labelSubmit: 'Submit Feedback'
});
Obviously, this has to be after you've already called Sentry.init(...) however you do it. You can set the labels however you want and read more about it in the documentation.
If you do it like this, the message "User has some feedback" (or whatever message you use) will appear in your Issues list in Sentry with a blue mark next to it instead of the orangish-red exceptions. That's helps to distinguish it.
I made my first acceptance test with Ember CLI. I use Ember Mirage to mock the server.
test('create file', function(assert){
visit('/login');
fillIn('input[name=username]', 'Joe');
fillIn('input[name=password]', 'foo');
click('button');
andThen(function() {
visit('/projects/files');
});
andThen(function(){
assert.ok(true);
})
});
The test runs successfully, but it hangs, and I am getting the following error
Uncaught (in promise) Error: Called stop() outside of a test context
at Object.stop (http://localhost:4200/assets/test-support.js:2469:10)
at Class.asyncStart (http://localhost:4200/assets/vendor.js:49507:13)
at asyncStart (http://localhost:4200/assets/vendor.js:41446:44) at
Object.async (http://localhost:4200/assets/vendor.js:41460:7) at
fulfill (http://localhost:4200/assets/vendor.js:61624:26) at
handleMaybeThenable (http://localhost:4200/assets/vendor.js:61584:9)
at resolve (http://localhost:4200/assets/vendor.js:61597:7) at sealed
(http://localhost:4200/assets/vendor.js:61536:11)
Ajax service
I use an ajax service, which makes calls to custom api endpoints. As you can see it uses the standard JSONAPISerializer. Could be still a problem ? This is an existing app, and there is no easy way to turn off this service, to test without it.
export default Ember.Service.extend({
// http://stackoverflow.com/questions/9705773/non-crud-actions-with-ember-data
call: function(method, type, id, action, hash = null){
var owner = Ember.getOwner(this);
var adapter = owner.lookup('adapter:application');
var url = adapter.buildURL(type, id) + '/' + action;
if (hash) {
hash.data = $.extend({}, hash);
}
return adapter.ajax(url, method, hash);
}
});
EDIT 1
I have changed the test slightly + turned on ENV.APP.LOG_TRANSITIONS_INTERNAL and ENV.APP.LOG_TRANSITIONS to see better whats going on:
$.Velocity.mock = true
var done = assert.async();
visit('/login');
fillIn('input[name=username]', 'Joe');
fillIn('input[name=password]', 'foo');
click('button');
andThen(function() {
visit('/projects/files/new/overview');
setTimeout(function() {
assert.equal( find('.btn-primary').length, 2,"button was found" );
done();
}, 20000);
});
It looks like the login works fine, then
Transition #2: projects.files.new.overview.index: calling deserialize
hook ember.debug.js:51061 Transition #2:
projects.files.new.overview.index: calling afterModel hook
ember.debug.js:51061 Transition #2: Resolved all models on destination
route; finalizing transition. ember.debug.js:6520 generated ->
controller:projects Object {fullName: "controller:projects"}
tells me, that the transition was ok, and I can see the new page in qunit's container.
sometimes I also receive
Uncaught Error: Assertion Failed: You have turned on testing mode,
which disabled the run-loop's autorun. You will need to wrap any code
with asynchronous side-effects in a run
When I request a single resource that returns a 404 (or 403 for that matter) Ember Data is creating a local record.
For example, I load my app from scratch at /items/123. The adapter does a request for GET /items/123 which results in a 404 but now I have an item record in my local store with id=123. All attributes are undefined expect where the model defines default values.
Also, all model flags are false except isValid which is true.
Is this excepted behaviour? It seems strange that the local record gets created even though the server is saying that it doesn't exist (or that the user is not allowed to see it).
Details
I'm running Ember 1.8.1 and Ember Data 1.0.0-beta.11.
Here's what I have for Routes—pretty basic stuff:
// itemsRoute, TOP LEVEL
model: function() {
return this.store.find('item');
}
// itemRoute, CHILD LEVEL
model: function(params) {
return this.store.find('item', params.item_id); // <-- this returns 404
},
actions: {
error: function(err) {
this.replaceWith('items'); // jump out to main list view
return true; // ensure error bubbles up
}
}
The rejection in the model hook is working because the error action is triggered and I'm redirected out the top level /items view. But I still end up with a local record for an item that doesn't exist.
After thinking about this a little more... try altering your error handler in itemRoute from...
actions: {
error: function(err) {
this.replaceWith('items'); // jump out to main list view
return true; // ensure error bubbles up
}
}
to...
actions: {
error: function(err) {
this.transitionTo('items'); // jump out to main list view
return false; // Stop event bubbling - we've handled it.
//You could also console.log parts or all of your error
//You can return true too if you want to keep the event bubbling.
}
}
I don't have a concrete reason for why this would be yet. Waiting to hear back from you if it worked.
This is most definitely a bug. It seems this was fixed a while back but as was reintroduced by a change on March 14.
I've submitted a bug report https://github.com/emberjs/data/issues/3085
First of all I don't use Ruby nor Devise :) (all my searches led me to plugins that kind of rely on Devise)
I want to do pretty simple authentication with Ember, I have a REST backend that blocks requests without a proper cookie(user-pass) and i want Ember to watch when it gets 403 forbidden (won't let you to transition into protected URLs) and then pop up a user-login dialog.
So when a user tries to send a new message for example(lets say i've built a forum) Ember will fire the request and if it gets 403 it will block the transition and popup a login form and will retry the transition after the login have completed
Also is there a way to get the errors from ember-data and respond to them? (if a user tries to change an attribute he can't access i would like to inform him about it[Access denied or something like that])
I want to use custom errors that my server will send to ember data not just error numbers but words like "Sorry you can't change this before 12 PM"
You can simply listen to the response of your server and transition to your LOGIN (or whatever you call it) route. In my apps I happen to keep two types of routes (LOGIN and AUTHENTICATED). When they access the authenticated routes without logging in, they get a 401 unauthorized error and get transitioned to the LOGIN route.
// AuthenticatedRoute.js
redirectToLogin: function(transition) {
// alert('You must log in!');
var loginController = this.controllerFor('login');
loginController.set('attemptedTransition', transition);
this.transitionTo('login');
},
events: {
error: function(reason, transition) {
if (reason.status === 401) {
this.redirectToLogin(transition);
} else {
console.log(reason);
window.alert('Something went wrong');
}
}
},
model: function () {
return this.store.find('post');
},
So now when the user requests for post he gets a 401 and gets transitioned to LOGIN controller.
// LoginController.js
login: function() {
var self = this, data = this.getProperties('username', 'password');
// Clear out any error messages.
this.set('errorMessage', null);
$.post('/login', data).then(function(response) {
self.set('errorMessage', response.message);
if (response.success) {
alert('Login succeeded!');
// Redirecting to the actual route the user tried to access
var attemptedTransition = self.get('attemptedTransition');
if (attemptedTransition) {
attemptedTransition.retry();
self.set('attemptedTransition', null);
} else {
// Redirect to 'defaultRoute' by default.
self.transitionToRoute('defaultRoute');
}
}
});
}
The basic answer you need is capturing the events in the route and transitioning accordingly. I just happened to include the code for attempted transition as it comes in handy at times.
ember-data.js: https://github.com/emberjs/data/tree/0396411e39df96c8506de3182c81414c1d0eb981
In short, when there is an error, I want to display error messages in the view, and then the user can 1) cancel, which will rollback the transaction 2) correct the input errors and successfully commit the transaction, passing the validations on the server.
Below is a code snippet from the source. It doesn't include an error callback.
updateRecord: function(store, type, record) {
var id = get(record, 'id');
var root = this.rootForType(type);
var data = {};
data[root] = this.toJSON(record);
this.ajax(this.buildURL(root, id), "PUT", {
data: data,
context: this,
success: function(json) {
this.didUpdateRecord(store, type, record, json);
}
});
},
Overall, what is the flow of receiving an error from the server and updating the view? It seems that an error callback should put the model in an isError state, and then the view can display the appropriate messages. Also, the transaction should stay dirty. That way, the transaction can use rollback.
It seems that using store.recordWasInvalid is going in the right direction, though.
This weekend I was trying to figure the same thing out. Going off what Luke said, I took a closer look at the ember-data source for the latest commit (Dec 11).
TLDR; to handle ember-data update/create errors, simply define becameError() and becameInvalid(errors) on your DS.Model instance. The cascade triggered by the RESTadapter's AJAX error callback will eventually call these functions you define.
Example:
App.Post = DS.Model.extend
title: DS.attr "string"
body: DS.attr "string"
becameError: ->
# handle error case here
alert 'there was an error!'
becameInvalid: (errors) ->
# record was invalid
alert "Record was invalid because: #{errors}"
Here's the full walk through the source:
In the REST adapter, the AJAX callback error function is given here:
this.ajax(this.buildURL(root, id), "PUT", {
data: data,
context: this,
success: function(json) {
Ember.run(this, function(){
this.didUpdateRecord(store, type, record, json);
});
},
error: function(xhr) {
this.didError(store, type, record, xhr);
}
});
didError is defined here and it in turn calls the store's recordWasInvalid or recordWasError depending on the response:
didError: function(store, type, record, xhr) {
if (xhr.status === 422) {
var data = JSON.parse(xhr.responseText);
store.recordWasInvalid(record, data['errors']);
} else {
store.recordWasError(record);
}
},
In turn, store.recordWasInvalid and store.recordWasError (defined here) call the record (a DS.Model)'s handlers. In the invalid case, it passes along error messages from the adapter as an argument.
recordWasInvalid: function(record, errors) {
record.adapterDidInvalidate(errors);
},
recordWasError: function(record) {
record.adapterDidError();
},
DS.Model.adapterDidInvalidate and adapterDidError (defined here) simply send('becameInvalid', errors) or send('becameError') which finally leads us to the handlers here:
didLoad: Ember.K,
didUpdate: Ember.K,
didCreate: Ember.K,
didDelete: Ember.K,
becameInvalid: Ember.K,
becameError: Ember.K,
(Ember.K is just a dummy function for returning this. See here)
So, the conclusion is, you simply need to define functions for becameInvalid and becameError on your model to handle these cases.
Hope this helps someone else; the docs certainly don't reflect this right now.
DS.RESTAdapter just got a bit more error handling in this commit but we are still not yet at a point where we have a great recommendation for error handling.
If you are ambitious/crazy enough to put apps in production today with ember-data (as I have been!), it is best to make sure that the likelihood of failures in your API is extremely low. i.e. validate your data client-side.
Hopefully, we can update this question with a much better answer in the coming months.
I just ran into such a situation, not sure if this is already explained anywhere.
I am using:
Em.VERSION : 1.0.0
DS.VERSION : "1.0.0-beta.6"
Ember Validations (dockyard) : Version: 1.0.0.beta.1
Ember I18n
The model was initially mixedin with Validation mixin.
App.Order = DS.Model.extend(Ember.Validations.Mixin, {
.....
someAttribute : DS.attr('string'),
/* Client side input validation with ember-validations */
validations : {
someAttribute : {
presence : {
message : Ember.I18n.t('translations.someAttributeInputError')
}
}
}
});
In the template, corresponding handlebars is added. (note that ember validations will automatically add errors to model.errors.<attribute> in case of input validations, I will be using same trade-off in server validations as well)
<p>{{t 'translations.myString'}}<br>
{{view Ember.TextField valueBinding="attributeName"}}
{{#if model.errors.attributeName.length}}<small class="error">{{model.errors.attributeName}}</small>{{/if}}
</p
Now, we will be saving the Order
App.get('order').save().then(function () {
//move to next state?
}, function(xhr){
var errors = xhr.responseJSON.errors;
for(var error in errors){ //this loop is for I18n
errors[error] = Ember.I18n.t(errors[error]);
}
controller.get('model').set('errors', errors); //this will overwrite current errors if any
});
Now if there is some validation error thrown from server, the returned packet being used is
{"errors":{"attributeName1":"translations.attributeNameEror",
"another":"translations.anotherError"}}
status : 422
It is important to use status 422
So this way, your attribute(s) can be validated client side and again on server side.
Disclaimer : I am not sure if this is the best way!
Since there's currently no good solution in stock Ember-Data, I made my own solution by adding an apiErrors property to DS.Model and then in my RestAdapter subclass (I already needed my own) I added error callbacks to the Ajax calls for createRecord and updateRecord that save the errors and put the model in the "invalid" state, which is supposed to mean client-side or server-side validations failed.
Here's the code snippets:
This can go in application.js or some other top-level file:
DS.Model.reopen({
// Added for better error handling on create/update
apiErrors: null
});
This goes in the error callbacks for createRecord and updateRecord in a RestAdapter subclass:
error: function(xhr, textStatus, err) {
console.log(xhr.responseText);
errors = null;
try {
errors = JSON.parse(xhr.responseText).errors;
} catch(e){} //ignore parse error
if(errors) {
record.set('apiErrors',errors);
}
record.send('becameInvalid');
}