Add missing rejection handling for Promises (#7008)
* Add eslint-plugin-promise to detect uncaught rejections * Move alert generation for errors to actions/alert * Add missing rejection handling for Promises * Use catch() instead of onReject on then() Then it will catches rejection from onFulfilled. This detection can be disabled by `allowThen` option, though.
This commit is contained in:
		@@ -13,6 +13,7 @@ plugins:
 | 
			
		||||
- react
 | 
			
		||||
- jsx-a11y
 | 
			
		||||
- import
 | 
			
		||||
- promise
 | 
			
		||||
 | 
			
		||||
parserOptions:
 | 
			
		||||
  sourceType: module
 | 
			
		||||
@@ -152,3 +153,5 @@ rules:
 | 
			
		||||
    - "app/javascript/**/__tests__/**"
 | 
			
		||||
  import/no-unresolved: error
 | 
			
		||||
  import/no-webpack-loader-syntax: error
 | 
			
		||||
 | 
			
		||||
  promise/catch-or-return: error
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ export function fetchAccount(id) {
 | 
			
		||||
      dispatch(importFetchedAccount(response.data));
 | 
			
		||||
    })).then(() => {
 | 
			
		||||
      dispatch(fetchAccountSuccess());
 | 
			
		||||
    }, error => {
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
      dispatch(fetchAccountFail(id, error));
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,10 @@
 | 
			
		||||
import { defineMessages } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
 | 
			
		||||
  unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const ALERT_SHOW    = 'ALERT_SHOW';
 | 
			
		||||
export const ALERT_DISMISS = 'ALERT_DISMISS';
 | 
			
		||||
export const ALERT_CLEAR   = 'ALERT_CLEAR';
 | 
			
		||||
@@ -22,3 +29,21 @@ export function showAlert(title, message) {
 | 
			
		||||
    message,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function showAlertForError(error) {
 | 
			
		||||
  if (error.response) {
 | 
			
		||||
    const { data, status, statusText } = error.response;
 | 
			
		||||
 | 
			
		||||
    let message = statusText;
 | 
			
		||||
    let title   = `${status}`;
 | 
			
		||||
 | 
			
		||||
    if (data.error) {
 | 
			
		||||
      message = data.error;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return showAlert(title, message);
 | 
			
		||||
  } else {
 | 
			
		||||
    console.error(error);
 | 
			
		||||
    return showAlert(messages.unexpectedTitle, messages.unexpectedMessage);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
import { CancelToken } from 'axios';
 | 
			
		||||
import { CancelToken, isCancel } from 'axios';
 | 
			
		||||
import { throttle } from 'lodash';
 | 
			
		||||
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
 | 
			
		||||
import { tagHistory } from '../settings';
 | 
			
		||||
import { useEmoji } from './emojis';
 | 
			
		||||
import { importFetchedAccounts } from './importer';
 | 
			
		||||
import { updateTimeline } from './timelines';
 | 
			
		||||
import { showAlertForError } from './alerts';
 | 
			
		||||
 | 
			
		||||
let cancelFetchComposeSuggestionsAccounts;
 | 
			
		||||
 | 
			
		||||
@@ -291,6 +292,10 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
 | 
			
		||||
  }).then(response => {
 | 
			
		||||
    dispatch(importFetchedAccounts(response.data));
 | 
			
		||||
    dispatch(readyComposeSuggestionsAccounts(token, response.data));
 | 
			
		||||
  }).catch(error => {
 | 
			
		||||
    if (!isCancel(error)) {
 | 
			
		||||
      dispatch(showAlertForError(error));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}, 200, { leading: true, trailing: true });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
import { importFetchedAccounts } from './importer';
 | 
			
		||||
import { showAlertForError } from './alerts';
 | 
			
		||||
 | 
			
		||||
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
 | 
			
		||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
 | 
			
		||||
@@ -236,7 +237,7 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
 | 
			
		||||
  api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
 | 
			
		||||
    dispatch(importFetchedAccounts(data));
 | 
			
		||||
    dispatch(fetchListSuggestionsReady(q, data));
 | 
			
		||||
  });
 | 
			
		||||
  }).catch(error => dispatch(showAlertForError(error)));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fetchListSuggestionsReady = (query, accounts) => ({
 | 
			
		||||
 
 | 
			
		||||
@@ -116,14 +116,11 @@ export function register () {
 | 
			
		||||
            pushNotificationsSetting.remove(me);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            getRegistration()
 | 
			
		||||
              .then(getPushSubscription)
 | 
			
		||||
              .then(unsubscribe);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
          return getRegistration()
 | 
			
		||||
            .then(getPushSubscription)
 | 
			
		||||
            .then(unsubscribe);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(console.warn);
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn('Your browser does not support Web Push Notifications.');
 | 
			
		||||
    }
 | 
			
		||||
@@ -143,6 +140,6 @@ export function saveSettings() {
 | 
			
		||||
      if (me) {
 | 
			
		||||
        pushNotificationsSetting.set(me, data);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    }).catch(console.warn);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
import { showAlertForError } from './alerts';
 | 
			
		||||
 | 
			
		||||
export const SETTING_CHANGE = 'SETTING_CHANGE';
 | 
			
		||||
export const SETTING_SAVE   = 'SETTING_SAVE';
 | 
			
		||||
@@ -23,7 +24,9 @@ const debouncedSave = debounce((dispatch, getState) => {
 | 
			
		||||
 | 
			
		||||
  const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
 | 
			
		||||
 | 
			
		||||
  api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
 | 
			
		||||
  api(getState).put('/api/web/settings', { data })
 | 
			
		||||
    .then(() => dispatch({ type: SETTING_SAVE }))
 | 
			
		||||
    .catch(error => dispatch(showAlertForError(error)));
 | 
			
		||||
}, 5000, { trailing: true });
 | 
			
		||||
 | 
			
		||||
export function saveSettings() {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ import { initReport } from '../actions/reports';
 | 
			
		||||
import { openModal } from '../actions/modal';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { boostModal, deleteModal } from '../initial_state';
 | 
			
		||||
import { showAlertForError } from '../actions/alerts';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
 | 
			
		||||
@@ -83,7 +84,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onEmbed (status) {
 | 
			
		||||
    dispatch(openModal('EMBED', { url: status.get('url') }));
 | 
			
		||||
    dispatch(openModal('EMBED', {
 | 
			
		||||
      url: status.get('url'),
 | 
			
		||||
      onError: error => dispatch(showAlertForError(error)),
 | 
			
		||||
    }));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onDelete (status) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ export default class EmbedModal extends ImmutablePureComponent {
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    url: PropTypes.string.isRequired,
 | 
			
		||||
    onClose: PropTypes.func.isRequired,
 | 
			
		||||
    onError: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +36,8 @@ export default class EmbedModal extends ImmutablePureComponent {
 | 
			
		||||
      iframeDocument.body.style.margin = 0;
 | 
			
		||||
      this.iframe.width  = iframeDocument.body.scrollWidth;
 | 
			
		||||
      this.iframe.height = iframeDocument.body.scrollHeight;
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
      this.props.onError(error);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,14 @@
 | 
			
		||||
import { defineMessages } from 'react-intl';
 | 
			
		||||
import { showAlert } from '../actions/alerts';
 | 
			
		||||
import { showAlertForError } from '../actions/alerts';
 | 
			
		||||
 | 
			
		||||
const defaultFailSuffix = 'FAIL';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
 | 
			
		||||
  unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default function errorsMiddleware() {
 | 
			
		||||
  return ({ dispatch }) => next => action => {
 | 
			
		||||
    if (action.type && !action.skipAlert) {
 | 
			
		||||
      const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
 | 
			
		||||
 | 
			
		||||
      if (action.type.match(isFail)) {
 | 
			
		||||
        if (action.error.response) {
 | 
			
		||||
          const { data, status, statusText } = action.error.response;
 | 
			
		||||
 | 
			
		||||
          let message = statusText;
 | 
			
		||||
          let title   = `${status}`;
 | 
			
		||||
 | 
			
		||||
          if (data.error) {
 | 
			
		||||
            message = data.error;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          dispatch(showAlert(title, message));
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error(action.error);
 | 
			
		||||
          dispatch(showAlert(messages.unexpectedTitle, messages.unexpectedMessage));
 | 
			
		||||
        }
 | 
			
		||||
        dispatch(showAlertForError(action.error));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,12 @@ const limit = 1024;
 | 
			
		||||
// https://webkit.org/status/#specification-service-workers
 | 
			
		||||
const asyncCache = window.caches ? caches.open('mastodon-system') : Promise.reject();
 | 
			
		||||
 | 
			
		||||
function printErrorIfAvailable(error) {
 | 
			
		||||
  if (error) {
 | 
			
		||||
    console.warn(error);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function put(name, objects, onupdate, oncreate) {
 | 
			
		||||
  return asyncDB.then(db => new Promise((resolve, reject) => {
 | 
			
		||||
    const putTransaction = db.transaction(name, 'readwrite');
 | 
			
		||||
@@ -77,7 +83,9 @@ function evictAccountsByRecords(records) {
 | 
			
		||||
 | 
			
		||||
    function evict(toEvict) {
 | 
			
		||||
      toEvict.forEach(record => {
 | 
			
		||||
        asyncCache.then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])));
 | 
			
		||||
        asyncCache
 | 
			
		||||
          .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
 | 
			
		||||
          .catch(printErrorIfAvailable);
 | 
			
		||||
 | 
			
		||||
        accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
 | 
			
		||||
 | 
			
		||||
@@ -90,11 +98,11 @@ function evictAccountsByRecords(records) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    evict(records);
 | 
			
		||||
  });
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function evictStatus(id) {
 | 
			
		||||
  return evictStatuses([id]);
 | 
			
		||||
  evictStatuses([id]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function evictStatuses(ids) {
 | 
			
		||||
@@ -110,7 +118,7 @@ export function evictStatuses(ids) {
 | 
			
		||||
      idIndex.getKey(id).onsuccess =
 | 
			
		||||
        ({ target }) => target.result && store.delete(target.result);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function evictStatusesByRecords(records) {
 | 
			
		||||
@@ -127,7 +135,9 @@ export function putAccounts(records) {
 | 
			
		||||
        const oldURL = target.result[key];
 | 
			
		||||
 | 
			
		||||
        if (newURL !== oldURL) {
 | 
			
		||||
          asyncCache.then(cache => cache.delete(oldURL));
 | 
			
		||||
          asyncCache
 | 
			
		||||
            .then(cache => cache.delete(oldURL))
 | 
			
		||||
            .catch(printErrorIfAvailable);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@@ -145,10 +155,14 @@ export function putAccounts(records) {
 | 
			
		||||
    oncomplete();
 | 
			
		||||
  }).then(records => {
 | 
			
		||||
    evictAccountsByRecords(records);
 | 
			
		||||
    asyncCache.then(cache => cache.addAll(newURLs));
 | 
			
		||||
  });
 | 
			
		||||
    asyncCache
 | 
			
		||||
      .then(cache => cache.addAll(newURLs))
 | 
			
		||||
      .catch(printErrorIfAvailable);
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function putStatuses(records) {
 | 
			
		||||
  put('statuses', records).then(evictStatusesByRecords);
 | 
			
		||||
  put('statuses', records)
 | 
			
		||||
    .then(evictStatusesByRecords)
 | 
			
		||||
    .catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -129,6 +129,7 @@
 | 
			
		||||
    "eslint": "^4.15.0",
 | 
			
		||||
    "eslint-plugin-import": "^2.8.0",
 | 
			
		||||
    "eslint-plugin-jsx-a11y": "^5.1.1",
 | 
			
		||||
    "eslint-plugin-promise": "^3.7.0",
 | 
			
		||||
    "eslint-plugin-react": "^7.5.1",
 | 
			
		||||
    "jest": "^21.2.1",
 | 
			
		||||
    "raf": "^3.4.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -2433,6 +2433,10 @@ eslint-plugin-jsx-a11y@^5.1.1:
 | 
			
		||||
    emoji-regex "^6.1.0"
 | 
			
		||||
    jsx-ast-utils "^1.4.0"
 | 
			
		||||
 | 
			
		||||
eslint-plugin-promise@^3.7.0:
 | 
			
		||||
  version "3.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.7.0.tgz#f4bde5c2c77cdd69557a8f69a24d1ad3cfc9e67e"
 | 
			
		||||
 | 
			
		||||
eslint-plugin-react@^7.5.1:
 | 
			
		||||
  version "7.5.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.5.1.tgz#52e56e8d80c810de158859ef07b880d2f56ee30b"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user