How can i handle http requests failures with ember.js? - ember.js

I've seen this question.
Until nothing is done for ember-data failures handling what can i write to patch it, so that i can have a message if my request doesn't have a 200OK response?
I'm looking at ember-data source code, for example, here's the createRecord function
createRecord: function(store, type, record) {
var root = this.rootForType(type);
var adapter = this;
var data = {};
data[root] = this.serialize(record, { includeId: true });
return this.ajax(this.buildURL(root), "POST", {
data: data
}).then(function(json){
Ember.run(adapter, 'didCreateRecord', store, type, record, json);
}, function(xhr) {
adapter.didError(store, type, record, xhr);
throw xhr;
});
}
I expected something like:
success: //do something
error: //do something else
But there's nothing like this. It's only a then after the ajax request.
What should I do? rewrite completely all the methods i need?

As of this commit ember data now uses promises (specifically the rsvp implementation), which replaces the success: and error: callbacks for a .then() style:
promise.then(function(value) {
// success
}, function(value) {
// failure
});
This is very similar to the previous callback style, but follows the promises style.
The first function is called on success and the second function is called on a failure. Looking at the code you posted shows that didError() is called on ajax failure, which is implemented in the adapter.
didError() (source) calls store.recordWasInvalid() if the status was 422, otherwise it calls store.recordWasError() (source).
store.recordWasInvalid() and store.recordWasError() transition the record into the isError or isValid = false state and triggers the becameError() and becameInvalid() events on the record. You can check these states or subscribe to these events to show your error message.

Related

Ember/RSVP Promise value changes when passed to resolve()

I am trying to wrap a jQuery AJAX request in a Ember RSVP promise, and I have this issue where the value I send to the resolve function (which is the jqXHR parameter) changes from object to string.
The code for doing the request + creating the request is as follows:
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.ajax([URL], {
type: 'POST',
dataType: 'text'
}).then((data, textStatus, jqXHR) => {
console.log(jqXHR);
resolve(jqXHR);
}, (jqXHR, textStatus, errorThrown) => {
resolve(jqXHR);
});
});
And in my controller, I handle the request like this:
promise.then((response) => {
console.log(response);
if (response.status === 200) {
something...
} else {
something else...
}
receipt.set('busy', false);
});
Now, from the basic (and probably flawed) comprehension I have of RSVP.Promise, the resolve(jqXHR) line should send the jqXHR object as parameter to the callback, right?
Problem is, when I print the response I get in the console, all I get is 200 success, which is the body of the HTTP request I do.
However, when I print the jqXHR before resolving it, it correctly prints the whole object:
Object {readyState: 4, responseText: "200 success", status: 200, statusText: "OK"}
So why is this happening? Is Ember doing some wierd black magic and converts the jqXHR object to string? Or is the data string being sent in lieu of the jqXHR object I am expecting?
Thanks!
Got it!
Alright, so apparently rsvp.js checks if the object sent to the resolve method is 'thenable' (read then-able), i.e. if it contains a then method.
If it is 'thenable', it will execute the then method of the object, take its first argument (only) and take it as the fulfilled value. This little magic allows chaining resolves but doesn't work very well with callbacks that take multiple arguments.
So in my case, I was sending jqXHR to be resolved, which does contain a then method (according to the jQuery documentation). RSVP then tried to fulfill the jqXHR object as a promise, and returned the first argument of the resolve callback.
Now, the signature for the jqXHR resolve callback is function(data, textStatus, jqXHR);, which is why the object sent to MY callback was not jqXHR as I expected, but was the first argument of the jqXHR resolve callback: data.
TL;DR: Check if the object you try to resolve your promise with has a then method, because it WILL be executed.

Ember-CLI-Mirage enforcing JSON:API?

Stumped on a couple failures and want to know if I'm understanding Mirage correctly:
1.In ember-cli-mirage, am I correct that the server response I define should reflect what my actual server is returning? For example:
this.get('/athletes', function(db, request) {
let athletes = db.athletes || [];
return {
athletes: athletes,
meta: { count: athletes.length }
}
});
I am using custom serializers and the above matches the format of my server response for a get request on this route, however, on two tests I'm getting two failures with this error: normalizeResponse must return a valid JSON API document: meta must be an object
2.Is mirage enforcing the json:api format, and is it doing so because of the way I'm setting up the tests?
For example, I have several tests that visit the above /athletes route, yet my failures occur when I use an async call like below. I would love to know the appropriate way to correctly overwrite the server response behavior, as well as why the normalizeResponse error appears in the console for 2 tests but only causes the one below to fail.
test('contact params not sent with request after clicking .showglobal', function(assert) {
assert.expect(2);
let done = assert.async();
server.createList('athlete', 10);
//perform a search, which shows all 10 athletes
visit('/athletes');
fillIn('.search-inner input', "c");
andThen(() => {
server.get('/athletes', (db, request) => {
assert.notOk(params.hasOwnProperty("contacts"));
done();
});
//get global athletes, which I thought would now be intercepted by the server.get call defined within the andThen block
click('button.showglobal');
});
});
Result:
✘ Error: Assertion Failed: normalizeResponse must return a valid JSON API document:
* meta must be an object
expected true
I tried changing my server response to a json:api format as suggested in the last example here but this looks nothing like my actual server response and causes my tests to fail since my app doesn't parse a payload with this structure. Any tips or advice must appreciated.
You are correct. Are the failures happening for the mock you've shown above? It looks to me like that would always return meta as an object, so verify the response is what you think it should be by looking in the console after the request is made.
If you'd like to see responses during a test, enter server.logging = true in your test:
test('I can view the photos', function() {
server.logging = true;
server.createList('photo', 10);
visit('/');
andThen(function() {
equal( find('img').length, 10 );
});
});
No, Mirage is agnostic about your particular backend, though it does come with some defaults. Again I would try enabling server.logging here to debug your tests.
Also, when writing asserts against the mock server, define the route handlers at the beginning of the test, as shown in the example from the docs.
I was able to get my second test to pass based on Sam's advice. My confusion was how to assert against the request params for a route that I have to visit and perform actions on. I was having to visit /athletes, click on different buttons, and each of these actions was sending separate requests (and params) to the /athletes route. That's is why I was trying to redefine the route handler within the andThen block (i.e. after I had already visited the route using the route definition in my mirage/config file).
Not in love with my solution, but the way I handled it was to move my assertion out of route handler and instead assign the value of the request to a top-level variable. That way, in my final andThen() block, I was able to assert against the last call to the /athletes route.
assert.expect(1);
//will get assigned the value of 'request' on each server call
let athletesRequest;
//override server response defined in mirage/config in order to
//capture and assert against request/response after user actions
server.get('athletes', (db, request) => {
let athletes = db.athletes || [];
athletesRequest = request;
return {
athletes: athletes,
meta: { count: athletes.length }
};
});
//sends request to /athletes
visit('/athletes');
andThen(() => {
//sends request to /athletes
fillIn('.search-inner input', "ab");
andThen(function() {
//sends (final) request to /athletes
click('button.search');
andThen(function() {
//asserts against /athletes request made on click('button.search') assert.notOk(athletesRequest.queryParams.hasOwnProperty("contact"));
});
});
});
I'm still getting console errors related to meta is not an object, but they are not preventing tests from passing. Using the server.logging = true allowed me to see that meta is indeed an object in all FakeServer responses.
Thanks again to Sam for the advice. server.logging = true and pauseTest() make acceptance tests a lot easier to troubleshoot.

How to implement a proper global HTTP error handling in ember

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.

Ember: how to retrieve validation errors after becameInvalid state?

I use the RESTadpater to persist data. When a validation error occurs, I want to return a 422 response and then log the errors and show an indication next to each incorrect field.
My REST response status code is as follows:
Status Code:422 Unprocessable Entity
My REST response body is as follows:
{
"message": "Validation failed",
"errors": [
{
"name": "duplicate"
}
]
}
In my controller, the becameInvalid fires correctly.
App.AuthorsNewController = Ember.ObjectController.extend({
startEditing: function () {
//Create a new record on a local transaction
this.transaction = this.get('store').transaction();
this.set('model', this.transaction.createRecord(App.Author, {}));
},
save: function (author) {
//Local commit - author record goes in Flight state
author.get('transaction').commit();
//If response is success: didCreate fires
//Transition to edit of the new record
author.one('didCreate', this, function () {
this.transitionToRoute('author.edit', author);
});
//If response is 422 (validation problem at server side): becameError fires
author.one('becameInvalid', this, function () {
console.log "Validation problem"
});
}
...
2 QUESTIONS:
I want to log below the 'console.log "Validation problem"', the complete list of errors returned by the server. How can I do that ?
In my hbs template, I want to indicate an error next to the relevant field. How can I do this ?
I am not sure that the data returned via REST adapter is correct. So problem might be at the REST side or at the Ember side ...
Solution:
In controller save function:
author.one('becameInvalid', this, function () {
console.log "Validation problem"
this.set('errors', this.get('content.errors'));
});
In hbs template:
{{view Ember.TextField valueBinding='name'}}
{{#if errors.name}}{{errors.name}}{{/if}}
Here is how I do it, may not be the best practice but it works for me:
instead of using commit(), I use save(), and if you wonder what's the difference, here is the link. I haven't tried your approach of using transaction, but basically I create the record using record = App.Model.createRecord(...), and here is the code of my apiAddShop function inside the AddShopController:
apiAddShop: function() {
//console.log("add shop");
newShop = App.Shop.createRecord({name:this.get('name'), currentUserRole:"owner"});
//this.get('store').commit(); // Use record.save() instead, then() is not defined for commit()
var self = this;
newShop.save().then(function(response){
self.transitionToRoute('shops');
}, function(response){
// if there is error:
// server should respond with JSON that has a root "errors"
// and with status code: 422
// otherwise the response could not be parsed.
var errors = response.errors;
for(var attr in errors){
if (self.hasOwnProperty(attr)) {
self.set(attr+"Error", true);
self.set(attr+"Message", Ember.String.classify(attr)+" "+errors[attr]);
}
console.log(attr + ': ' + errors[attr]);
}
console.log(response.errors.name[0]);
});
},
the above code assume there is a attrError(boolean) and attrMessage(string) for each of the attributes in your form. Then in your template, you could bind the class of your field to these error attributes, such as <div {{bindAttr class=":control-group nameError:error:"}}>, and the error message could be easily display next to the form field such as: <span {{bindAttr class=":help-inline nameError::hidden"}} id="new_shop_error">{{nameMessage}}</span> Here is my example handlebar gist (have to use gist here, since SO is escaping my html inputs).

How should errors be handled when using the Ember.js Data RESTAdapter?

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');
}