Commit 2f591b3d authored by Nathan Friend's avatar Nathan Friend

Suppress AJAX errors caused by browser navigation

This commit attempts to detect when errors are caused due to AJAX
requests being cancelled when the user navigates to a new page. If this
is the case, we swallow the error so that an error message isn't shown
to the user a split second before they navigate away from the page.
parent 61df1924
import axios from 'axios';
import csrf from './csrf';
import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
// Used by Rails to check if it is a valid XHR request
......@@ -25,6 +26,20 @@ axios.interceptors.response.use(
},
);
let isUserNavigating = false;
window.addEventListener('beforeunload', () => {
isUserNavigating = true;
});
// Ignore AJAX errors caused by requests
// being cancelled due to browser navigation
const { gon } = window;
const featureFlagEnabled = gon && gon.features && gon.features.suppressAjaxNavigationErrors;
axios.interceptors.response.use(
response => response,
err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating, featureFlagEnabled),
);
export default axios;
/**
......
/**
* An Axios error interceptor that suppresses AJAX errors caused
* by the request being cancelled when the user navigates to a new page
*/
export default (err, isUserNavigating, featureFlagEnabled) => {
if (featureFlagEnabled && isUserNavigating && err.code === 'ECONNABORTED') {
// If the user is navigating away from the current page,
// prevent .then() and .catch() handlers from being
// called by returning a Promise that never resolves
return new Promise(() => {});
}
// The error is not related to browser navigation,
// so propagate the error
return Promise.reject(err);
};
---
title: Suppress error messages shown when navigating to a new page
merge_request: 17706
author:
type: fixed
......@@ -100,6 +100,8 @@ describe('ProductivityApp component', () => {
describe('user has access to the group', () => {
beforeEach(() => {
wrapper.vm.$store.state.charts.charts[chartKeys.main].errorCode = null;
return wrapper.vm.$nextTick();
});
describe('when the main chart is loading', () => {
......
......@@ -38,6 +38,10 @@ module Gitlab
gon.current_user_fullname = current_user.name
gon.current_user_avatar_url = current_user.avatar_url
end
# Initialize gon.features with any flags that should be
# made globally available to the frontend
push_frontend_feature_flag(:suppress_ajax_navigation_errors, default_enabled: true)
end
# Exposes the state of a feature flag to the frontend code.
......
import suppressAjaxErrorsDuringNavigation from '~/lib/utils/suppress_ajax_errors_during_navigation';
import waitForPromises from 'helpers/wait_for_promises';
describe('suppressAjaxErrorsDuringNavigation', () => {
const OTHER_ERR_CODE = 'foo';
const NAV_ERR_CODE = 'ECONNABORTED';
it.each`
isFeatureFlagEnabled | isUserNavigating | code
${false} | ${false} | ${OTHER_ERR_CODE}
${false} | ${false} | ${NAV_ERR_CODE}
${false} | ${true} | ${OTHER_ERR_CODE}
${false} | ${true} | ${NAV_ERR_CODE}
${true} | ${false} | ${OTHER_ERR_CODE}
${true} | ${false} | ${NAV_ERR_CODE}
${true} | ${true} | ${OTHER_ERR_CODE}
`('should return a rejected Promise', ({ isFeatureFlagEnabled, isUserNavigating, code }) => {
const err = { code };
const actual = suppressAjaxErrorsDuringNavigation(err, isUserNavigating, isFeatureFlagEnabled);
return expect(actual).rejects.toBe(err);
});
it('should return a Promise that never resolves', () => {
const err = { code: NAV_ERR_CODE };
const actual = suppressAjaxErrorsDuringNavigation(err, true, true);
const thenCallback = jest.fn();
const catchCallback = jest.fn();
actual.then(thenCallback).catch(catchCallback);
return waitForPromises().then(() => {
expect(thenCallback).not.toHaveBeenCalled();
expect(catchCallback).not.toHaveBeenCalled();
});
});
});
......@@ -236,8 +236,15 @@ describe('Frequent Items App Component', () => {
.then(() => {
expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
})
// This test waits for multiple ticks in order to allow the responses to
// propagate through each interceptor installed on the Axios instance.
// This shouldn't be necessary; this test should be refactored to avoid this.
// https://gitlab.com/gitlab-org/gitlab/issues/32479
.then(vm.$nextTick)
.then(vm.$nextTick)
.then(vm.$nextTick)
.then(() => {
expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
mockSearchedProjects.length,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment