/
kreyndelHam
/
Konstructorium
Обзор
Документация
Войти
/
kreyndelHam
/
Konstructorium
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
dev
frontend/src/utils/userError.js
332 строки
10 KB
MihahamYT
Fix + gigachat ai
24 май 2026, 13:00
24 май 2026, 13:00
a8d7bbd
Код
Авторство
О чём код?
import { defaultLanguage, t as translate } from '../services/translations' import { normalizeApiPath } from './normalizeApiPath' const INCIDENT_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' /** Machine-readable codes must not be shown as the main user message. */ const MACHINE_CODE_PATTERN = /^(API_|NETWORK_|CLIENT_)[A-Z0-9_]+$/ /** Map generated error_code prefixes to i18n keys. */ const ERROR_CODE_PATTERN_KEYS = [ ['API_NOT_FOUND_404', 'errors.patterns.notFound'], ['API_AUTH_401', 'errors.patterns.unauthorized'], ['API_FORBIDDEN_403', 'errors.patterns.forbidden'], ['API_PAYLOAD_TOO_LARGE_413', 'errors.patterns.payloadTooLarge'], ['API_VALIDATION_', 'errors.patterns.validation'], ['API_SERVER_5XX', 'errors.patterns.server'], ['API_ERROR_', 'errors.patterns.generic'], ['NETWORK_OFFLINE', 'errors.patterns.network'], ['CLIENT_UNHANDLED', 'errors.patterns.client'], ] /** * Return true when text looks like an internal error code, not user copy. * * @param {string} value * @returns {boolean} */ export const isMachineErrorCode = (value) => { if (!value || typeof value !== 'string') { return false } return MACHINE_CODE_PATTERN.test(value.trim().toUpperCase()) } /** * Resolve translation or null when key is missing. * * @param {string} key * @param {(key: string, params?: Record<string, string>) => string} t * @param {Record<string, string>} [params] * @returns {string|null} */ export const translateKeyOrNull = (key, t, params = {}) => { const translated = t(key, params) return translated && translated !== key ? translated : null } /** * Generate support incident id (KON-XXXXXXXX). * * @returns {string} */ export const createIncidentId = () => { let suffix = '' for (let index = 0; index < 8; index += 1) { const randomIndex = Math.floor(Math.random() * INCIDENT_ALPHABET.length) suffix += INCIDENT_ALPHABET[randomIndex] } return `KON-${suffix}` } /** * Extract machine-readable error code from API payload. * * @param {unknown} payload * @returns {string|null} */ export const extractBackendErrorCode = (payload) => { if (!payload || typeof payload !== 'object') { return null } const data = /** @type {Record<string, unknown>} */ (payload) const raw = data.code || data.error_code || data.errorCode if (typeof raw === 'string' && raw.trim()) { return raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, '_').slice(0, 64) } return null } /** * Extract the first field-level validation message from DRF-style errors object. * * @param {Record<string, unknown>} errors * @returns {string|null} */ export const extractFirstFieldErrorMessage = (errors) => { if (!errors || typeof errors !== 'object') { return null } if (Array.isArray(errors.non_field_errors) && errors.non_field_errors[0]) { return String(errors.non_field_errors[0]) } const firstEntry = Object.entries(errors)[0] if (!firstEntry) { return null } const [field, value] = firstEntry const message = Array.isArray(value) ? value[0] : value if (message === undefined || message === null || message === '') { return null } return `${field}: ${message}` } /** * Extract human-readable message from heterogeneous API error bodies. * * Field-level ``errors`` are preferred over generic envelope ``message`` so users * see actionable validation text (e.g. registration) instead of boilerplate. * * @param {unknown} payload * @returns {string|null} */ export const extractBackendErrorMessage = (payload) => { if (!payload) { return null } if (typeof payload === 'string') { return payload.trim() || null } if (typeof payload !== 'object') { return null } const data = /** @type {Record<string, unknown>} */ (payload) if (typeof data.detail === 'string' && data.detail.trim()) { return data.detail.trim() } if (typeof data.error === 'string' && data.error.trim()) { return data.error.trim() } if (data.errors && typeof data.errors === 'object') { const fieldMessage = extractFirstFieldErrorMessage( /** @type {Record<string, unknown>} */ (data.errors), ) if (fieldMessage) { return fieldMessage } } if (typeof data.message === 'string' && data.message.trim()) { return data.message.trim() } return null } /** * Build deterministic client error code when backend does not provide one. * * @param {{status?: number, apiService?: string, source?: string}} context * @returns {string} */ export const resolveErrorCode = ({ status = 0, apiService = '', source = 'api' }) => { if (source === 'client') { return 'CLIENT_UNHANDLED' } if (source === 'network' || status === 0) { return 'NETWORK_OFFLINE' } const serviceToken = (apiService || 'unknown').toUpperCase().replace(/[^A-Z0-9]/g, '_') if (status === 401) { return `API_AUTH_401_${serviceToken}` } if (status === 403) { return `API_FORBIDDEN_403_${serviceToken}` } if (status === 404) { return `API_NOT_FOUND_404_${serviceToken}` } if (status === 413) { return `API_PAYLOAD_TOO_LARGE_413_${serviceToken}` } if (status >= 500) { return `API_SERVER_5XX_${serviceToken}` } if (status >= 400) { return `API_VALIDATION_${status}_${serviceToken}` } return `API_ERROR_${status || 'UNKNOWN'}_${serviceToken}` } /** * Translate error code via i18n function. * * @param {string} errorCode * @param {(key: string) => string} t * @returns {string|null} */ export const translateErrorCode = (errorCode, t) => { if (!errorCode || !t) { return null } const exact = translateKeyOrNull(`errors.codes.${errorCode}`, t) if (exact) { return exact } const normalizedCode = errorCode.toUpperCase() for (const [prefix, patternKey] of ERROR_CODE_PATTERN_KEYS) { if (normalizedCode.startsWith(prefix)) { const patternMessage = translateKeyOrNull(patternKey, t) if (patternMessage) { return patternMessage } } } return null } /** * Decide if backend message is safe to show to end users. * * @param {string|null} message * @param {string} errorCode * @returns {boolean} */ export const isUsableBackendMessage = (message, errorCode) => { if (!message || typeof message !== 'string') { return false } const trimmed = message.trim() if (!trimmed) { return false } if (isMachineErrorCode(trimmed)) { return false } if (trimmed.toUpperCase() === errorCode.toUpperCase()) { return false } if (/^Request failed with status code \d+$/i.test(trimmed)) { return false } return true } /** * Normalize any thrown value into structured user error info. * * @param {unknown} error * @param {(key: string, params?: Record<string, string>) => string} t * @returns {{ * incidentId: string, * errorCode: string, * userMessage: string, * source: string, * statusCode: number|null, * apiService: string, * path: string, * }} */ export const normalizeUserError = (error, t) => { const incidentId = createIncidentId() const path = typeof window !== 'undefined' ? window.location.pathname : '' if (error && typeof error === 'object' && error.__userErrorNormalized) { return error.__userErrorNormalized } let statusCode = null let apiService = '' let source = 'client' let backendPayload = null let backendMessage = null let backendCode = null const axiosError = /** @type {{response?: {status?: number, data?: unknown}, config?: {baseURL?: string, url?: string}, message?: string, code?: string}} */ ( error ) if (axiosError?.response) { source = 'api' statusCode = axiosError.response.status ?? null backendPayload = axiosError.response.data backendMessage = extractBackendErrorMessage(backendPayload) backendCode = extractBackendErrorCode(backendPayload) const requestUrl = axiosError.config?.baseURL ? `${axiosError.config.baseURL}${axiosError.config.url || ''}` : axiosError.config?.url || '' if (requestUrl) { const normalized = normalizeApiPath(requestUrl) apiService = normalized.apiService } } else if (axiosError?.code === 'ERR_NETWORK' || !navigator.onLine) { source = 'network' } else if (typeof error === 'string') { backendMessage = error source = 'client' } else if (error instanceof Error) { backendMessage = error.message source = 'client' } const errorCode = backendCode || resolveErrorCode({ status: statusCode || 0, apiService, source }) const translated = translateErrorCode(errorCode, t) const fallbackKey = source === 'network' ? 'errors.fallback.network' : 'errors.fallback.generic' const fallbackMessage = translateKeyOrNull(fallbackKey, t) || translateKeyOrNull(fallbackKey, (key, params) => translate(key, defaultLanguage, params)) || translateKeyOrNull(fallbackKey, (key, params) => translate(key, 'en', params)) const userMessage = (isUsableBackendMessage(backendMessage, errorCode) ? backendMessage.trim() : null) || translated || fallbackMessage const normalized = { incidentId, errorCode, userMessage: String(userMessage).slice(0, 500), source, statusCode, apiService, path, } if (error && typeof error === 'object') { error.__userErrorNormalized = normalized } return normalized } /** * Format toast body with incident reference for the user. * * @param {{userMessage: string, errorCode: string, incidentId: string}} info * @param {(key: string, params?: Record<string, string>) => string} t * @returns {string} */ export const formatUserErrorToast = (info, t) => { const codeLine = t('errors.support.codeLine', { code: info.errorCode }) const incidentLine = t('errors.support.incidentLine', { incidentId: info.incidentId }) return `${info.userMessage}\n\n${codeLine}\n${incidentLine}` }