GrResponseErrorBanner
Берут, когда запрос упал.
Обзор
Когда брать
- запрос упал — сеть, 500, 422 с ошибками полей: тип ошибки определяется сам и меняет заголовок;
- ошибок по полям несколько — они печатаются списком с подписями вместо одной общей фразы;
- запрос можно повторить —
canRetryдаёт кнопку рядом с сообщением; - нужен статус HTTP — бейдж кода помогает поддержке, не мешая обычному пользователю.
Когда взять другое
| Нужно | Берите |
|---|---|
| Сообщение не про ответ сервера | GrAlert |
| Короткое уведомление о результате | GrToaster |
| Ошибка одного поля формы | GrFormField |
| Ошибка внутри диалога | GrConfirmDialog / GrPromptDialog |
| Данных нет, но это не ошибка | GrEmptyState |
Варианты все тона и виды — настоящим компонентом
tone
primaryneutralsuccesswarningdangerinfoslateazure
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrResponseErrorBanner :error="error" />Примеры 3 у каждого — живое демо и его код
Ответ 422, разложенный по полям
useResponseErrorпринимает самResponse: цепочка парсеров узнаёт формат Laravel, род ошибки становится «validation» с тоном предупреждения, а у каждого поля своя строка.field-labelsпревращает ключи API в слова — «customer.vat_id» становится «Номером НДС».Invoice INV-0418 · customer details Applies from the next invoiceInvoices already sent keep the details they were issued with.Отчёт, который не загрузился
Упавший
fetchклассифицируется как «network»: тон опасности и сcan-retryкнопка повтора — повтор вполне может сработать.retryвозвращает ошибку, и страница выполняет тот же запрос снова.Revenue by planThe report is built on request.
Отказ в формате RFC 7807
Ответ
application/problem+jsonразбирает свой пресет: заголовок и подробности становятся сообщением. 403 — ошибка «client», повтор ничего не изменит, поэтому баннер не предлагает повторить — только причина и бейдж статуса для поддержки.Members · Northwind Traders Removing a member signs them out on every device. Invoices they created stay.
MOMina OrtizOwner · [email protected]JWJonas WeberAdmin · [email protected]
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
tone |
| undefined | Жёсткий оверрайд тона. Бьёт `toneByKind`. |
texts | Partial<ResponseErrorTexts> | undefined | {} | Частичный override текстов. Мерджится с `DEFAULT_RESPONSE_ERROR_TEXTS`. |
toneByKind |
| {} | Маппинг `kind` → `tone`. Мерджится с `DEFAULT_TONE_BY_KIND`. |
showDetails | boolean | undefined | true | Показывать ли блок `details`/`fieldErrors` под основным сообщением. |
showFieldLabels | boolean | undefined | true | Префиксить ли `field:` перед сообщениями по полям. |
fieldLabels | Record<string, string> | undefined | {} | Человекочитаемые подписи полей (`{ file: 'Файл' }`). |
dedupeDetails | boolean | undefined | true | Удалять дубликаты сообщений между `message` и `details`. |
canRetry | boolean | undefined | false | Показывать ли кнопку «Повторить». |
canDismiss | boolean | undefined | true | Показывать ли кнопку «Скрыть». |
autoHideKinds | ResponseErrorKind[] | undefined | DEFAULT_AUTO_HIDE_KINDS | Какие `kind` не рендерить вообще (баннер скрывается). |
showStatus | boolean | undefined | true | Показывать ли HTTP-status бейдж. |
testIdPrefix | string | undefined | "response-error" | Префикс `data-testid`. |
errorобязательный | ResponseErrorInfo | null | — | Готовая структура ошибки. Если `null` — баннер не рендерится. |
События
| Event | Сигнатура | Описание |
|---|---|---|
retry | (event: "retry", error: ResponseErrorInfo): void | — |
dismiss | (event: "dismiss"): void | — |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { UseResponseErrorOptions, UseResponseErrorReturn, ResponseErrorParserPresets } from '@feugene/granularity/components/GrResponseErrorBanner'Файлdist/types/components/GrResponseErrorBanner/useResponseError.d.ts
Опции для useResponseError. Все поля опциональны и поддерживают
**реактивные геттеры** (() => value) — чтобы пробрасывать пропсы
родителя без потери реактивности.
type UseResponseErrorOptions = {
/** Цепочка парсеров. По умолчанию — `defaultResponseErrorParsers`. */
parsers?: () => ResponseErrorParser[] | undefined;Публичный возвращаемый API.
type UseResponseErrorReturn = {
/** Текущее состояние ошибки. `null` — баннер скрыт. */
currentError: Ref<ResponseErrorInfo | null>;
/** `currentError.value !== null`. */
isVisible: ComputedRef<boolean>;
/** Последний «сырой» вход (для retry/логов). */
lastRaw: Ref<unknown>;
/**
* Главное API: классифицирует raw, сохраняет в state, возвращает info
* (или `null`, если `kind` в `autoHideKinds`). Возвращает Promise,
* т.к. нормализатор поддерживает `Response.json()` (async).
*/
setRaw: (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo | null>;Stateful-композабл вокруг createResponseErrorClassifier. Хранит
currentError, инкапсулирует setRaw/retry/dismiss. Подходит для типичного
сценария «поймал ошибку запроса → показал баннер → ретрай/скрыть».
function useResponseError(options?: UseResponseErrorOptions): UseResponseErrorReturn;Файлdist/types/components/GrResponseErrorBanner/parsers/createResponseErrorClassifier.d.ts
Универсальное «ядро» парсеров — транспортно-независимый набор, безопасный
для любого бэкенда (Laravel / Symfony / Go / …). Не делает формат-специфичных
допущений: разбирает только отмену, сетевые ошибки, HTTP-статус и «плоское»
сообщение из тела. Используется как дефолт в useDialogService.
Порядок: abort → network → httpStatus → plainMessage.
const coreResponseErrorParsers: ResponseErrorParser[];Именованные пресеты-строительные блоки для удобной композиции цепочки
парсеров без ручного импорта каждого из них.
- core — универсальное ядро (см. coreResponseErrorParsers).
- all — полная дефолтная цепочка (defaultResponseErrorParsers).
- остальные — серверо-специфичные парсеры, подключаемые осознанно.
type ResponseErrorParserPresets = {
core: ResponseErrorParser[];
all: ResponseErrorParser[];
laravel: ResponseErrorParser;
problemDetails: ResponseErrorParser;
jsonApi: ResponseErrorParser;
fileValidation: ResponseErrorParser;
};Добавляет пользовательские парсеры в начало дефолтной цепочки.
function extendDefaultParsers(extra: ResponseErrorParser[]): ResponseErrorParser[];type CreateResponseErrorClassifierOptions = {
parsers?: ResponseErrorParser[];
texts?: Partial<ResponseErrorTexts>;
/** Имя поля основного сообщения в body (для `plainMessageParser`). */
messageKey?: string;
};Фабрика классификатора. Возвращает функцию (raw, meta?) => Promise<ResponseErrorInfo>,
которая:
1. нормализует вход через normalizeError,
2. строит ResponseErrorContext,
3. прогоняет цепочку парсеров (с поддержкой stop),
4. собирает финальный ResponseErrorInfo с фолбэками из texts.
function createResponseErrorClassifier(options?: CreateResponseErrorClassifierOptions): (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo>;
export declare function resolveResponseErrorTitle(kind: ResponseErrorKind, texts: ResponseErrorTexts): string;Файлdist/types/components/GrResponseErrorBanner/parsers/normalizeError.d.ts
Универсально приводит «сырой» вход к {@link NormalizedRawError}.
Поддерживаемые источники:
- **axios** — AxiosError с response.status/response.data/response.headers,
либо request без response + code === 'ERR_NETWORK', либо code === 'ERR_CANCELED'.
- **fetch Response** — объект Response с ok=false. Тело читается через
clone().json() (с фолбэком на .text()); поэтому функция асинхронная.
- **fetch TypeError** — TypeError('Failed to fetch') ⇒ isNetwork=true.
- **{ response: Response, body: unknown }** — если потребитель уже прочитал тело сам.
- **XMLHttpRequest** / { status, responseText, response } — пара полей.
- **AbortError / DOMException** ⇒ isAbort=true.
- **Error/строка/произвольный объект** — кладётся в body/raw как есть.
Цель — отделить «откуда пришла ошибка» от «как её классифицировать»: парсеры
работают только с нормализованным ResponseErrorContext, не зная о транспорте.
function normalizeError(raw: unknown): Promise<NormalizedRawError>;Файлdist/types/components/GrResponseErrorBanner/responseError.types.d.ts
Категория ошибки. Жёстко разделены по семантике (см. DEFAULT_TONE_BY_KIND).
- network — транспортная проблема: нет интернета, DNS, CORS-preflight,
fetch TypeError, axios ERR_NETWORK. **Ответа от сервера нет.**
- aborted — запрос отменён клиентом (AbortController, axios CanceledError).
- validation — ошибка валидации формы/файлов; есть структурированные поля или
статус 400/422. UX: «исправь поле и повтори».
- client — прочие 4xx (auth/forbidden/not-found/conflict/rate-limit) —
проблема запроса, retry обычно не помогает.
- server — 5xx, проблема на сервере; retry разумен.
- unknown — ничего не сматчилось, fallback.
type ResponseErrorKind = 'network' | 'aborted' | 'validation' | 'client' | 'server' | 'unknown';Визуальный тон баннера — та же шкала, что у всех компонентов пакета.
type ResponseErrorTone = GrTone;Ошибка по конкретному полю (Laravel-style / JSON:API pointer / RFC 7807 errors).
type ResponseErrorFieldError = {
/** Имя поля. Для Laravel — ключ из `errors`. Для JSON:API — последний сегмент `source.pointer`. */
field: string;
/** Сообщения, относящиеся к этому полю. */
messages: string[];
};Готовая к показу структура. Это то, что баннер принимает в error-проп.
type ResponseErrorInfo = {
kind: ResponseErrorKind;
/** HTTP-статус, если известен. */
status?: number;
/** Основной заголовок-сообщение для пользователя. */
message: string;
/** Плоский список дополнительных сообщений под `message`. */
details?: string[];
/** Структурированные ошибки по полям (если сервер вернул JSON-объект `errors`). */
fieldErrors?: ResponseErrorFieldError[];
/** Оригинальный объект ошибки/ответа (для логов/retry). */
raw: unknown;
/** Произвольный контекст, переданный потребителем (например, `{ files }`). */
meta?: Record<string, unknown>;
/**
* `message` не пришёл ни от одного парсера, и классификатор подставил свой
* текст по `kind`. Флаг нужен баннеру: только такое сообщение он вправе
* заменить переводом. Опознание фолбэка сравнением строк давало ложное
* срабатывание на сервере, вернувшем дословно тот же текст.
*/
isFallbackMessage?: boolean;
};Нормализованный «сырой» результат запроса. Заполняется normalizeError
единообразно для axios / fetch / xhr / голого Error / строки.
type NormalizedRawError = {
/** Оригинал, как пришёл от потребителя. */
raw: unknown;
/** HTTP-статус (если был ответ). */
status?: number;
/** Уже распарсенное тело ответа (JSON-объект, строка). */
body?: unknown;
/** Заголовки ответа (lower-case keys). */
headers?: Record<string, string>;
/** Признак отмены (`AbortError` / axios `isCancel`). */
isAbort?: boolean;
/** Признак транспортной ошибки без ответа. */
isNetwork?: boolean;
};Контекст, который классификатор передаёт каждому парсеру.
raw — исходный объект (для duck-typing).
status/body/headers/isAbort/isNetwork — нормализованные поля.
meta — произвольный пользовательский контекст (например, { files, formId }).
texts — мёрджнутые тексты, парсер может использовать дефолты как fallback.
type ResponseErrorContext = NormalizedRawError & {
meta: Record<string, unknown>;
texts: ResponseErrorTexts;
};Результат работы парсера. Любое поле опционально — классификатор сливает
результаты по очереди (последний непустой выигрывает).
stop: true — прервать цепочку, никакие следующие парсеры не вызовутся.
type ResponseErrorParserResult = {
kind?: ResponseErrorKind;
status?: number;
message?: string;
details?: string[];
fieldErrors?: ResponseErrorFieldError[];
stop?: boolean;
};Сигнатура парсера.
type ResponseErrorParser = (ctx: ResponseErrorContext) => ResponseErrorParserResult | null | undefined;
/**
* Тексты-фолбэки для каждого `kind` + лейблы кнопок.
* Все строки переопределяются через проп `texts` (частичный мердж).
*/
export type ResponseErrorTexts = {Тексты-фолбэки для каждого kind + лейблы кнопок.
Все строки переопределяются через проп texts (частичный мердж).
type ResponseErrorTexts = {
networkTitle: string;
networkMessage: string;
abortedTitle: string;
abortedMessage: string;
validationTitle: string;
validationMessage: string;
clientTitle: string;
clientMessage: string;
serverTitle: string;
serverMessage: string;
unknownTitle: string;
unknownMessage: string;
/** Подпись бейджа HTTP-статуса. `{status}` подставляется компонентом. */
statusLabel: string;
retryLabel: string;
dismissLabel: string;
};Файлdist/types/components/GrResponseErrorBanner/GrFormErrorBanner.vue.d.ts
interface GrFormErrorBannerProps {
error: ResponseErrorInfo | null;
fieldLabels?: Record<string, string>;
texts?: Partial<ResponseErrorTexts>;
tone?: ResponseErrorTone;
toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
canRetry?: boolean;
canDismiss?: boolean;
autoHideKinds?: ResponseErrorKind[];
testIdPrefix?: string;
}interface GrFormErrorBannerEmits {
(e: 'retry', error: ResponseErrorInfo): void;
(e: 'dismiss'): void;
}Файлdist/types/components/GrResponseErrorBanner/GrResponseErrorBanner.vue.d.ts
interface GrResponseErrorBannerProps {
/** Готовая структура ошибки. Если `null` — баннер не рендерится. */
error: ResponseErrorInfo | null;
/** Частичный override текстов. Мерджится с `DEFAULT_RESPONSE_ERROR_TEXTS`. */
texts?: Partial<ResponseErrorTexts>;
/** Жёсткий оверрайд тона. Бьёт `toneByKind`. */
tone?: ResponseErrorTone;
/** Маппинг `kind` → `tone`. Мерджится с `DEFAULT_TONE_BY_KIND`. */
toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
/** Показывать ли блок `details`/`fieldErrors` под основным сообщением. */
showDetails?: boolean;
/** Префиксить ли `field:` перед сообщениями по полям. */
showFieldLabels?: boolean;
/** Человекочитаемые подписи полей (`{ file: 'Файл' }`). */
fieldLabels?: Record<string, string>;
/** Удалять дубликаты сообщений между `message` и `details`. */
dedupeDetails?: boolean;
/** Показывать ли кнопку «Повторить». */
canRetry?: boolean;
/** Показывать ли кнопку «Скрыть». */
canDismiss?: boolean;
/** Какие `kind` не рендерить вообще (баннер скрывается). */
autoHideKinds?: ResponseErrorKind[];
/** Показывать ли HTTP-status бейдж. */
showStatus?: boolean;
/** Префикс `data-testid`. */
testIdPrefix?: string;
}interface GrResponseErrorBannerEmits {
/** Нажата «Повторить» (`canRetry`); приходит текущая ошибка — повторить запрос, что её дал. */
(e: 'retry', error: ResponseErrorInfo): void;
/** Нажат крестик (`canDismiss`). Сам баннер не прячется: обнулить `error` — забота экрана. */
(e: 'dismiss'): void;
}Файлdist/types/components/GrResponseErrorBanner/GrUploadErrorBanner.vue.d.ts
interface GrUploadErrorBannerProps {
error: ResponseErrorInfo | null;
/** Файлы, которые пытались загрузить — пробрасываются в `retry` payload. */
files?: File[];
texts?: Partial<ResponseErrorTexts>;
tone?: ResponseErrorTone;
toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
canRetry?: boolean;
canDismiss?: boolean;
autoHideKinds?: ResponseErrorKind[];
testIdPrefix?: string;
}interface GrUploadErrorBannerEmits {
(e: 'retry', payload: {
error: ResponseErrorInfo;
files: File[];
}): void;
(e: 'dismiss'): void;
}Использование из прозы библиотеки
Три слоя
normalizeError— приводит axios /fetch Response/XMLHttpRequest/ голыйError/ строку к общему виду: статус, разобранное тело, заголовки, признаки отмены и сетевой ошибки. ТелоResponseчитается клоном, поэтому потребитель может прочитать его сам;- цепочка парсеров — от нормализованного контекста к
ResponseErrorInfo(kind,message,details,fieldErrors). Порядок значим, парсер может остановить цепочку (stop); - баннер — показ: тон по
kind, тексты из локали, дедупликация деталей.
Цепочка парсеров
По умолчанию — универсальное ядро (coreResponseErrorParsers): abort, сеть,
HTTP-статус, plain-сообщение. Оно работает на транспортном уровне и не делает
допущений о формате тела, поэтому не срабатывает ложно на «почти-Laravel»
ответах.
Серверо-специфичные парсеры подключаются осознанно:
const { setRaw } = useResponseError({
parsers: () => extendDefaultParsers([myParser]),
})
// или через builder в опциях диалогов и баннера
errorParsers: presets => [...presets.core, presets.laravel]Готовые пресеты: laravel, problemDetails (RFC 7807), jsonApi,
fileValidation (клиентская проверка файлов из GrFileUpload).
Тексты, i18n и флаг фолбэка
Тексты берутся из локали (gr.responseError.*), поверх — проп texts.
Подпись бейджа статуса — тот же механизм: statusLabel со вставкой
{status}.
Когда ни один парсер не дал сообщения, его подставляет классификатор и
помечает isFallbackMessage: true. Баннер заменяет переводом только такое
сообщение. Опознание фолбэка сравнением строк, которое здесь было раньше,
выбрасывало ответ сервера, если тот дословно совпал с дефолтом (а
"Network error." сервер вернуть вполне может).
Отсюда контракт парсера: message заполняется только тем, что нашлось в
ответе. Общий текст по kind — работа классификатора, и парсеру не надо его
подставлять; иначе сообщение приедет без флага, и перевести его будет уже
нельзя. Парсеры, знающие только тип ошибки (httpStatus, abort, network),
сообщение не заполняют вовсе.
Сообщение транспортной ошибки (Request failed with status 500 у axios)
сообщением сервера не считается: оно берётся, только если ответа не было
вовсе. Иначе пользователь видел бы техническую фразу на английском вместо
переведённого текста.
Тон и доступность
kind → тон по умолчанию: validation/client — warning, network/
server/unknown — danger, aborted — info. Перебивается точечно
(toneByKind) или целиком (tone).
Роль наследуется от GrAlert: warning и danger объявляются как
role="alert" (перебивают речь диктора), остальные — role="status" (ждут
паузы). Отдельной настройки у баннера нет намеренно — она есть у GrAlert.
autoHideKinds прячет баннер целиком для перечисленных kind: типовой случай
— тихо проглатывать aborted.
Пресеты
GrFormErrorBanner и GrUploadErrorBanner — тонкие обёртки с преднастройками:
формам не нужен повтор и нужны подписи полей, загрузке нужен повтор и контекст
файлов (его пресет отдаёт в retry как { error, files } — базовый баннер
отдаёт саму ошибку).
Своей разметки они не добавляют и живут в папке базового компонента осознанно: отдельная единица гранулярности не даст ничего — ни своего CSS, ни своего safelist, — а стоить будет двух лишних entry в сборке. Импортируются из корня пакета.
Пропы этого примера