mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
fix(ui): нормализовать detail из ответов API перед рендером — краш на 422
Что: все обработчики ошибок, которые клали сырой response.data.detail в
состояние и затем в JSX, переведены на существующий хелпер
getApiErrorMessage (src/utils/api-error.ts):
- Login.tsx — Telegram-auth, вход/регистрация по email, восстановление пароля
- Profile.tsx — смена email (запрос и подтверждение кода), повторная
отправка письма верификации
- ConnectedAccounts.tsx — привязка email к аккаунту
- admin/AnalyticsTab.tsx, admin/MenuEditorTab.tsx — сохранение настроек
Почему: на невалидный по схеме запрос FastAPI/Pydantic отвечает 422, где
detail — не строка, а массив объектов {type, loc, msg, input, ctx}.
Компоненты рендерили его как есть, React падал с «Objects are not valid
as a React child», и вся страница (включая логин) уходила в ErrorBoundary
«Something went wrong» — пользователь не видел причину отказа вообще.
Зачем: getApiErrorMessage сводит Pydantic-массив к строке
«field: message; ...», поэтому вместо краша форма показывает настоящий
текст ошибки валидации. Ветвления по detail.includes(...) сохранены и
работают по нормализованной строке — поведение прежнее для строковых
detail, но безопасное для любой формы ответа.
This commit is contained in:
@@ -9,10 +9,11 @@ import { useAuthStore } from '../store/auth';
|
||||
import { displayName } from '../utils/displayName';
|
||||
import { authApi } from '../api/auth';
|
||||
import { isValidEmail } from '../utils/validation';
|
||||
import { getApiErrorMessage } from '../utils/api-error';
|
||||
import {
|
||||
notificationsApi,
|
||||
NotificationSettings,
|
||||
NotificationSettingsUpdate,
|
||||
type NotificationSettings,
|
||||
type NotificationSettingsUpdate,
|
||||
} from '../api/notifications';
|
||||
import { referralApi } from '../api/referral';
|
||||
import { brandingApi, type EmailAuthEnabled } from '../api/branding';
|
||||
@@ -112,8 +113,8 @@ export default function Profile() {
|
||||
setError(null);
|
||||
setVerificationResendCooldown(UI.RESEND_COOLDOWN_SEC);
|
||||
},
|
||||
onError: (err: { response?: { data?: { detail?: string } } }) => {
|
||||
setError(err.response?.data?.detail || t('common.error'));
|
||||
onError: (err: unknown) => {
|
||||
setError(getApiErrorMessage(err, t('common.error')));
|
||||
setSuccess(null);
|
||||
},
|
||||
});
|
||||
@@ -133,13 +134,13 @@ export default function Profile() {
|
||||
setResendCooldown(UI.RESEND_COOLDOWN_SEC);
|
||||
}
|
||||
},
|
||||
onError: (err: { response?: { data?: { detail?: string } } }) => {
|
||||
const detail = err.response?.data?.detail;
|
||||
if (detail?.includes('already registered') || detail?.includes('already in use')) {
|
||||
onError: (err: unknown) => {
|
||||
const detail = getApiErrorMessage(err, '');
|
||||
if (detail.includes('already registered') || detail.includes('already in use')) {
|
||||
setChangeError(t('profile.changeEmail.emailAlreadyUsed'));
|
||||
} else if (detail?.includes('same as current')) {
|
||||
} else if (detail.includes('same as current')) {
|
||||
setChangeError(t('profile.changeEmail.sameEmail'));
|
||||
} else if (detail?.includes('rate limit') || detail?.includes('too many')) {
|
||||
} else if (detail.includes('rate limit') || detail.includes('too many')) {
|
||||
setChangeError(t('profile.changeEmail.tooManyRequests'));
|
||||
} else {
|
||||
setChangeError(detail || t('common.error'));
|
||||
@@ -157,11 +158,11 @@ export default function Profile() {
|
||||
// Note: auth user lives in the zustand store, not in React Query —
|
||||
// the explicit setUser above IS the refresh. No ['user'] query exists.
|
||||
},
|
||||
onError: (err: { response?: { data?: { detail?: string } } }) => {
|
||||
const detail = err.response?.data?.detail;
|
||||
if (detail?.includes('invalid') || detail?.includes('wrong')) {
|
||||
onError: (err: unknown) => {
|
||||
const detail = getApiErrorMessage(err, '');
|
||||
if (detail.includes('invalid') || detail.includes('wrong')) {
|
||||
setChangeError(t('profile.changeEmail.invalidCode'));
|
||||
} else if (detail?.includes('expired')) {
|
||||
} else if (detail.includes('expired')) {
|
||||
setChangeError(t('profile.changeEmail.codeExpired'));
|
||||
} else {
|
||||
setChangeError(detail || t('common.error'));
|
||||
|
||||
Reference in New Issue
Block a user