1
0
mirror of https://github.com/flarum/core.git synced 2025-07-23 01:31:40 +02:00

Improve client XHR error handling

The default XHR error handler produce an alert which is appropriate to the response status code. It can be overridden per-request (by specifying the `errorHandler` option) so that the alert can be suppressed or displayed in a different position (e.g. inside a modal).

ref #118
This commit is contained in:
Toby Zerner
2015-10-20 12:48:26 +10:30
parent 0952651cf3
commit f2dbb96e84
26 changed files with 192 additions and 175 deletions

View File

@@ -206,10 +206,14 @@ export default class App {
try {
return JSON.parse(responseText);
} catch (e) {
throw new RequestError(e.message, responseText);
throw new RequestError(500, responseText);
}
});
options.errorHandler = options.errorHandler || (error => {
throw error;
});
// When extracting the data from the response, we can check the server
// response code and show an error message to the user if something's gone
// awry.
@@ -225,26 +229,56 @@ export default class App {
const status = xhr.status;
if (status >= 500 && status <= 599) {
throw new RequestError('Internal Server Error', responseText);
if (status < 200 || status > 299) {
throw new RequestError(status, responseText, xhr);
}
return responseText;
};
this.alerts.dismiss(this.requestErrorAlert);
if (this.requestError) this.requestError.hideAlert();
// Now make the request. If it's a failure, inspect the error that was
// returned and show an alert containing its contents.
return m.request(options).then(null, error => {
if (error instanceof RequestError) {
this.alerts.show(this.requestErrorAlert = new Alert({
type: 'error',
children: 'Oops! Something went wrong. Please reload the page and try again.',
controls: app.forum.attribute('debug') ? [
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button>
] : undefined
}));
this.requestError = error;
let children;
switch (error.status) {
case 422:
children = error.response.errors
.map(error => [error.detail, <br/>])
.reduce((a, b) => a.concat(b), [])
.slice(0, -1);
break;
case 401:
case 403:
children = 'You do not have permission to do that.';
break;
case 404:
case 410:
children = 'The requested resource was not found.';
break;
default:
children = 'Oops! Something went wrong. Please reload the page and try again.';
}
error.alert = new Alert({
type: 'error',
children,
controls: app.forum.attribute('debug') ? [
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button>
] : undefined
});
try {
options.errorHandler(error);
} catch (error) {
this.alerts.show(error.alert);
}
throw error;
@@ -264,16 +298,18 @@ export default class App {
/**
* Show alert error messages for each error returned in an API response.
*
* @param {Array} errors
* @param {Object} response
* @public
*/
alertErrors(errors) {
errors.forEach(error => {
this.alerts.show(new Alert({
type: 'error',
children: error.detail
}));
});
alertErrors(response) {
if (response.errors) {
response.errors.forEach(error => {
this.alerts.show(new Alert({
type: 'error',
children: error.detail
}));
});
}
}
/**

View File

@@ -117,10 +117,11 @@ export default class Model {
*
* @param {Object} attributes The attributes to save. If a 'relationships' key
* exists, it will be extracted and relationships will also be saved.
* @param {Object} [options]
* @return {Promise}
* @public
*/
save(attributes) {
save(attributes, options = {}) {
const data = {
type: this.data.type,
id: this.data.id,
@@ -153,11 +154,11 @@ export default class Model {
this.pushData(data);
return app.request({
return app.request(Object.assign({
method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data: {data}
}).then(
}, options)).then(
// If everything went well, we'll make sure the store knows that this
// model exists now (if it didn't already), and we'll push the data that
// the API returned into the store.
@@ -181,17 +182,18 @@ export default class Model {
* Send a request to delete the resource.
*
* @param {Object} data Data to send along with the DELETE request.
* @param {Object} [options]
* @return {Promise}
* @public
*/
delete(data) {
delete(data, options = {}) {
if (!this.exists) return m.deferred.resolve().promise;
return app.request({
return app.request(Object.assign({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data
}).then(() => {
}, options)).then(() => {
this.exists = false;
this.store.remove(this);
});

View File

@@ -26,15 +26,16 @@ export default class Session {
*
* @param {String} identification The username/email.
* @param {String} password
* @param {Object} [options]
* @return {Promise}
* @public
*/
login(identification, password) {
return app.request({
login(identification, password, options = {}) {
return app.request(Object.assign({
method: 'POST',
url: app.forum.attribute('baseUrl') + '/login',
data: {identification, password}
})
}, options))
.then(() => window.location.reload());
}

View File

@@ -79,10 +79,11 @@ export default class Store {
* Alternatively, if an object is passed, it will be handled as the
* `query` parameter.
* @param {Object} [query]
* @param {Object} [options]
* @return {Promise}
* @public
*/
find(type, id, query = {}) {
find(type, id, query = {}, options = {}) {
let data = query;
let url = app.forum.attribute('apiUrl') + '/' + type;
@@ -94,11 +95,11 @@ export default class Store {
url += '/' + id;
}
return app.request({
return app.request(Object.assign({
method: 'GET',
url,
data
}).then(this.pushPayload.bind(this));
}, options)).then(this.pushPayload.bind(this));
}
/**

View File

@@ -109,27 +109,28 @@ export default class Modal extends Component {
}
/**
* Show an alert describing errors returned from the API, and give focus to
* Stop loading.
*/
loaded() {
this.loading = false;
m.redraw();
}
/**
* Show an alert describing an error returned from the API, and give focus to
* the first relevant field.
*
* @param {Object} response
* @param {RequestError} error
*/
handleErrors(response) {
const errors = response && response.errors;
if (errors) {
this.alert = new Alert({
type: 'error',
children: errors.map((error, k) => [error.detail, k < errors.length - 1 ? m('br') : ''])
});
}
onerror(error) {
this.alert = error.alert;
m.redraw();
if (errors) {
this.$('form [name=' + errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
if (error.status === 422 && error.response.errors) {
this.$('form [name=' + error.response.errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
} else {
this.$('form :input:first').select();
this.onready();
}
}
}

View File

@@ -5,10 +5,6 @@ export default class RequestErrorModal extends Modal {
return 'RequestErrorModal Modal--large';
}
title() {
return this.props.error.message;
}
content() {
let responseText;

View File

@@ -1,6 +1,15 @@
export default class RequestError {
constructor(message, responseText) {
this.message = message;
constructor(status, responseText, xhr) {
this.status = status;
this.responseText = responseText;
this.xhr = xhr;
try {
this.response = JSON.parse(responseText);
} catch (e) {
this.response = null;
}
this.alert = null;
}
}