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:
@@ -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
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,10 +5,6 @@ export default class RequestErrorModal extends Modal {
|
||||
return 'RequestErrorModal Modal--large';
|
||||
}
|
||||
|
||||
title() {
|
||||
return this.props.error.message;
|
||||
}
|
||||
|
||||
content() {
|
||||
let responseText;
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user