fix: бесконечный ретрай upload 400, скобки в именах файлов, тост batch-create
- useOfflineSync: tryUploadFile/trySendAttachmentMessage бросали ошибку без кода статуса — постоянная 400 не распознавалась regex \b400\b и вложение ретраилось бесконечно (спам POST /api/upload каждые ~30 сек). - validateFilename: разрешены скобки () в именах (типично для фото с телефонов; сохраняемое имя всё равно генерируется), в текст 400 добавлено само имя файла для диагностики. - BatchCreateDialog: тост ошибки показывает текст сервера (из BadRequestError или JSON в message 5xx) вместо общего «Ошибка создания задач».
This commit is contained in:
@@ -67,6 +67,22 @@ function getRemainingLockMinutes(startedAt: number): number {
|
|||||||
return Math.ceil(remaining / 60_000);
|
return Math.ceil(remaining / 60_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Достаём читабельный текст ошибки: apiRequest бросает BadRequestError/ConflictError
|
||||||
|
// с текстом сервера в message, а для 5xx message имеет вид "500: {json}".
|
||||||
|
function extractErrorMessage(e: unknown, fallback: string): string {
|
||||||
|
if (!(e instanceof Error) || e instanceof TypeError) return fallback;
|
||||||
|
const m = e.message || '';
|
||||||
|
const jsonStart = m.indexOf('{');
|
||||||
|
if (jsonStart >= 0) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(m.slice(jsonStart));
|
||||||
|
if (typeof parsed.error === 'string' && parsed.error) return parsed.error;
|
||||||
|
} catch { /* не JSON — показываем как есть */ }
|
||||||
|
}
|
||||||
|
if (m && m !== 'bad_request' && m !== 'conflict' && m !== 'billing_blocked') return m;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
export function BatchCreateDialog({ formId, open, onOpenChange, onSuccess }: BatchCreateDialogProps) {
|
export function BatchCreateDialog({ formId, open, onOpenChange, onSuccess }: BatchCreateDialogProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [selectedType, setSelectedType] = useState('');
|
const [selectedType, setSelectedType] = useState('');
|
||||||
@@ -264,7 +280,11 @@ export function BatchCreateDialog({ formId, open, onOpenChange, onSuccess }: Bat
|
|||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/dashboard'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/dashboard'] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Ошибка создания задач', variant: 'destructive' });
|
toast({
|
||||||
|
title: 'Ошибка создания задач',
|
||||||
|
description: extractErrorMessage(e, 'Проверьте соединение и попробуйте ещё раз'),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,9 @@ async function tryUploadFile(
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({}));
|
const err = await res.json().catch(() => ({}));
|
||||||
throw new Error(err.error || `Ошибка загрузки: ${file.name}`);
|
// Код статуса в сообщении обязателен: вызывающий код отличает постоянные
|
||||||
|
// ошибки (400/401/403 → не ретраить) от временных по паттерну `\b400\b` и т.п.
|
||||||
|
throw new Error(`${res.status}: ${err.error || `Ошибка загрузки: ${file.name}`}`);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return {
|
return {
|
||||||
@@ -293,7 +295,7 @@ async function trySendAttachmentMessage(
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({}));
|
const err = await res.json().catch(() => ({}));
|
||||||
throw new Error(err.error || 'Ошибка отправки сообщения');
|
throw new Error(`${res.status}: ${err.error || 'Ошибка отправки сообщения'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function getFileExt(originalname: string): string {
|
|||||||
// Validates original filename:
|
// Validates original filename:
|
||||||
// - extension must be in EXT_TO_MIME (whitelist)
|
// - extension must be in EXT_TO_MIME (whitelist)
|
||||||
// - name part blocks path traversal, shell metacharacters and control characters
|
// - name part blocks path traversal, shell metacharacters and control characters
|
||||||
// - Unicode letters (including Cyrillic), digits, spaces, dots, underscores and hyphens are allowed
|
// - Unicode letters (including Cyrillic), digits, spaces, dots, underscores, hyphens and parentheses are allowed
|
||||||
export function validateFilename(originalname: string): { valid: boolean; reason?: string } {
|
export function validateFilename(originalname: string): { valid: boolean; reason?: string } {
|
||||||
const basename = path.basename(originalname);
|
const basename = path.basename(originalname);
|
||||||
const ext = path.extname(basename).slice(1).toLowerCase();
|
const ext = path.extname(basename).slice(1).toLowerCase();
|
||||||
@@ -81,9 +81,10 @@ export function validateFilename(originalname: string): { valid: boolean; reason
|
|||||||
return { valid: false, reason: 'Пустое имя файла' };
|
return { valid: false, reason: 'Пустое имя файла' };
|
||||||
}
|
}
|
||||||
// Block path traversal, shell metacharacters, and control characters
|
// Block path traversal, shell metacharacters, and control characters
|
||||||
// Allow Unicode letters (including Cyrillic), digits, spaces, dots, underscores, hyphens
|
// Allow Unicode letters (including Cyrillic), digits, spaces, dots, underscores, hyphens, parentheses
|
||||||
if (/[\/\\&|;$()<>\`\"'\x00-\x1f]/.test(namePart) || namePart.includes('..')) {
|
// (скобки безопасны: сохраняемое имя файла генерируется, оригинальное используется только для отображения)
|
||||||
return { valid: false, reason: 'Имя файла содержит недопустимые символы' };
|
if (/[\/\\&|;$<>\`\"'\x00-\x1f]/.test(namePart) || namePart.includes('..')) {
|
||||||
|
return { valid: false, reason: `Имя файла содержит недопустимые символы: «${namePart.slice(0, 100)}»` };
|
||||||
}
|
}
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user