Untitled
Anonymous
plain_text
02/27/2026 4:26 AM
13.5 KB
18
Indexable
import {
AxiosInstance,
AxiosRequestConfig,
AxiosError,
AxiosResponse,
} from 'axios';
import Router from 'next/router';
import { dispatchGlobal } from '@otten-ecommerce/store';
interface InterceptorModel {
axios: AxiosInstance;
handleRTO?: boolean;
isErrorNoRemoveToken?: boolean;
hideBlocker?: boolean;
noRedirect?: boolean;
cardLoader?: boolean;
loaderClaim?: boolean;
}
// Request batch tracking for simple majority logic
const BATCH_WINDOW_MS = 2000; // 2 seconds window for a batch
const BATCH_IDLE_MS = 500; // 500ms idle time to consider batch complete
const FAILURE_THRESHOLD = 0.5; // Show modal if >50% of requests failed
interface RequestBatch {
successCount: number;
failedCount: number;
pendingCount: number;
lastActivityTime: number;
hasEvaluated: boolean;
failedMethods: string[]; // Track methods of failed requests
}
let requestBatch: RequestBatch = {
successCount: 0,
failedCount: 0,
pendingCount: 0,
lastActivityTime: 0,
hasEvaluated: false,
failedMethods: [],
};
let batchEvaluateTimer: ReturnType<typeof setTimeout> | null = null;
/**
* Reset batch counters for a new batch of requests
*/
const resetBatch = () => {
requestBatch = {
successCount: 0,
failedCount: 0,
pendingCount: 0,
lastActivityTime: Date.now(),
hasEvaluated: false,
failedMethods: [],
};
};
/**
* Evaluate the batch and show modal if failure rate exceeds threshold
*/
const evaluateBatch = () => {
if (requestBatch.hasEvaluated) {
return;
}
const total = requestBatch.successCount + requestBatch.failedCount;
// Only evaluate if we have some requests completed
if (total === 0) {
return;
}
const failureRate = requestBatch.failedCount / total;
// Show modal only if majority failed
if (failureRate > FAILURE_THRESHOLD) {
requestBatch.hasEvaluated = true;
// Determine error type from the most recent failure context
// For now, we'll defer to the error handler to dispatch with actual type
return true; // Signal that modal should be shown
}
return false;
};
/**
* Schedule batch evaluation after idle time
*/
const scheduleBatchEvaluation = (
callback: (shouldShow: boolean) => void
) => {
if (batchEvaluateTimer) {
clearTimeout(batchEvaluateTimer);
}
batchEvaluateTimer = setTimeout(() => {
batchEvaluateTimer = null;
// Check if still active (no new requests in IDLE time)
const idleTime = Date.now() - requestBatch.lastActivityTime;
if (idleTime >= BATCH_IDLE_MS && requestBatch.pendingCount === 0) {
const shouldShow = evaluateBatch();
if (!shouldShow && !requestBatch.hasEvaluated) {
// Batch passed threshold, prepare for new batch
resetBatch();
}
callback(shouldShow || false);
}
}, BATCH_IDLE_MS);
};
/**
* Track request start
*/
const trackRequestStart = () => {
const now = Date.now();
// Start new batch if window expired
if (
requestBatch.lastActivityTime > 0 &&
now - requestBatch.lastActivityTime > BATCH_WINDOW_MS
) {
resetBatch();
}
requestBatch.pendingCount++;
requestBatch.lastActivityTime = now;
};
/**
* Track request success
*/
const trackRequestSuccess = () => {
requestBatch.successCount++;
requestBatch.pendingCount--;
requestBatch.lastActivityTime = Date.now();
scheduleBatchEvaluation(() => {});
};
/**
* Track request error - returns error type for batch evaluation
*/
const trackRequestError = (
errorType: 'no-internet' | 'server-error' | null,
method: string
): { shouldShow: boolean; batchErrorType: 'no-internet' | 'server-error' | null } => {
requestBatch.pendingCount--;
requestBatch.lastActivityTime = Date.now();
if (!errorType) {
return { shouldShow: false, batchErrorType: null };
}
requestBatch.failedCount++;
requestBatch.failedMethods.push(method);
// Both no-internet and server-error defer to batch evaluation
scheduleBatchEvaluation((shouldShow) => {
if (shouldShow && errorType) {
// Use the most common failed method, or GET as default
const methodCounts = requestBatch.failedMethods.reduce(
(acc, m) => {
acc[m] = (acc[m] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
const mostCommonMethod = Object.entries(methodCounts).sort(
(a, b) => b[1] - a[1]
)[0]?.[0] || 'GET';
dispatchGlobal({
type: 'setModalError',
payload: {
isShow: true,
type: errorType,
method: mostCommonMethod,
},
});
}
});
return { shouldShow: false, batchErrorType: errorType };
};
export default (props: InterceptorModel) => {
const {
axios,
handleRTO,
isErrorNoRemoveToken,
hideBlocker,
noRedirect,
cardLoader,
loaderClaim
} = props;
const blocker = document.getElementById('blocker') as HTMLElement;
axios.interceptors.request.use(
(configAxios: AxiosRequestConfig) => {
if (cardLoader) {
window.dispatchEvent(new CustomEvent('show-card-loader'));
}
if (loaderClaim) {
window.dispatchEvent(new CustomEvent('show-claim-loader'));
}
if (!hideBlocker) {
blocker?.classList.add('active');
document.body.classList.add('hidden');
}
// Track request start for batch evaluation
trackRequestStart();
return configAxios;
},
(errorAxios: AxiosError) => {
if (cardLoader) {
window.dispatchEvent(new CustomEvent('hide-card-loader'));
}
if (loaderClaim) {
window.dispatchEvent(new CustomEvent('hide-claim-loader'));
}
return Promise.reject(errorAxios);
}
);
axios.interceptors.response.use(
(responseAxios: any) => {
if (cardLoader) {
window.dispatchEvent(new CustomEvent('hide-card-loader'));
}
if (loaderClaim) {
window.dispatchEvent(new CustomEvent('hide-claim-loader'));
}
if (!hideBlocker) {
blocker?.classList.remove('active');
document.body.classList.remove('hidden');
}
// Track request success
trackRequestSuccess();
return responseAxios;
},
(errorAxios: AxiosError) => {
if (cardLoader) {
window.dispatchEvent(new CustomEvent('hide-card-loader'));
}
if (loaderClaim) {
window.dispatchEvent(new CustomEvent('hide-claim-loader'));
}
if (!hideBlocker) {
blocker?.classList.remove('active');
document.body.classList.remove('hidden');
}
const errorCode = errorAxios?.response?.status;
const pathname = location.pathname;
const fullUrl = `${pathname}${location.search}`;
const errorType = resolveModalErrorType(errorAxios);
const requestMethod = errorAxios?.config?.method?.toUpperCase() || 'GET';
if (errorType) {
// Track error - modal will be shown by batch evaluator if threshold exceeded
trackRequestError(errorType, requestMethod);
}
// (disabled) needed updated flow
// if (handleRTO && errorAxios.code === "ECONNABORTED") {
// Router.push("/server-busy");
// }
if (errorCode === 401 && window.location.pathname !== '/login') {
if (!isErrorNoRemoveToken) localStorage.removeItem('access_token');
if (!noRedirect) {
if (
localStorage.getItem('history_path') === '/' ||
localStorage.getItem('history_path') === ''
) {
localStorage.removeItem('history_path');
Router.push('/');
} else {
Router.push({ pathname: '/login', query: { next: fullUrl } });
}
}
}
return Promise.reject(errorAxios);
}
);
};
const resolveModalErrorType = (
errorAxios: AxiosError
): 'no-internet' | 'server-error' | null => {
const errorCode = errorAxios?.code;
const errorMessage = String(errorAxios?.message || '').toLowerCase();
const statusCode = errorAxios?.response?.status;
const requestStatus = Number((errorAxios as any)?.request?.status || 0);
const resolvedStatusCode =
typeof statusCode === 'number'
? statusCode
: requestStatus > 0
? requestStatus
: undefined;
const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
const hasNoResponse = !errorAxios?.response;
if (typeof resolvedStatusCode === 'number' && resolvedStatusCode >= 500) {
return 'server-error';
}
if (errorCode === 'ERR_INTERNET_DISCONNECTED') {
return 'no-internet';
}
if (
isLikelyCorsError({
errorAxios,
isOnline,
hasNoResponse,
errorCode,
errorMessage,
requestStatus,
})
) {
return null;
}
if (errorCode === 'ERR_NETWORK') {
return isOnline ? 'server-error' : 'no-internet';
}
if (hasNoResponse && !isOnline) {
return 'no-internet';
}
if (errorCode === 'ECONNABORTED') {
return isOnline ? 'server-error' : 'no-internet';
}
if (hasNoResponse && errorMessage.includes('network error')) {
return isOnline ? 'server-error' : 'no-internet';
}
return null;
};
const isLikelyCorsError = ({
errorAxios,
isOnline,
hasNoResponse,
errorCode,
errorMessage,
requestStatus,
}: {
errorAxios: AxiosError;
isOnline: boolean;
hasNoResponse: boolean;
errorCode?: string;
errorMessage: string;
requestStatus: number;
}): boolean => {
if (!isOnline || !hasNoResponse) {
return false;
}
const isNetworkLikeError =
errorCode === 'ERR_NETWORK' || errorMessage.includes('network error');
if (!isNetworkLikeError || requestStatus !== 0) {
return false;
}
if (typeof window === 'undefined') {
return false;
}
const requestUrl = resolveRequestUrl(errorAxios?.config);
if (!requestUrl) {
return false;
}
try {
return new URL(requestUrl).origin !== window.location.origin;
} catch {
return false;
}
};
const resolveRequestUrl = (config?: AxiosRequestConfig): string | null => {
const rawUrl = config?.url;
if (!rawUrl || typeof rawUrl !== 'string') {
return null;
}
try {
const base =
config?.baseURL ||
(typeof window !== 'undefined' ? window.location.origin : undefined);
return base ? new URL(rawUrl, base).toString() : rawUrl;
} catch {
return rawUrl;
}
};
Editor is loading...
Leave a Comment