GrResponseErrorBanner
A universal generic banner for server or network response errors: it takes a raw error/response, runs it through a chain of parsers (HTTP statuses, Laravel/JSON:API/RFC 7807 validation, file/network/abort) and renders a title, main message, an optional list of per-field details and retry/dismiss actions — without knowing anything about a specific feature (file upload, forms, transactions).
Overview
When to take it
- the request failed — the network, a 500, a 422 with field errors: the type of the error is determined by itself and changes the heading;
- there are several errors by field — they are printed as a list with labels instead of one general phrase;
- the request can be retried —
canRetrygives a button next to the message; - the HTTP status is needed — a badge with the code helps support without getting in the way of an ordinary user.
When to take something else
| Need | Take |
|---|---|
| The message is not about a server answer | GrAlert |
| A short notification about a result | GrToaster |
| The error of a single form field | GrFormField |
| An error inside a dialog | GrConfirmDialog / GrPromptDialog |
| There is no data, but that is not an error | GrEmptyState |
Variants every tone and view, rendered by the real component
tone
primaryneutralsuccesswarningdangerinfoslateazure
The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrResponseErrorBanner :error="error" />Examples 3 each with a live demo and its code
A 422 from the API, field by field
useResponseErrortakes theResponseitself: the chain of parsers recognises the Laravel shape, the kind becomes «validation» with a warning tone, and each field gets its own line.field-labelsturns API keys into words — «customer.vat_id» becomes «VAT number».Invoice INV-0418 · customer details Applies from the next invoiceInvoices already sent keep the details they were issued with.A report that did not load
A failed
fetchis classified as «network»: a danger tone and, withcan-retry, a retry button — repeating may well work.retryhands the error back, and the page runs the same request again.Revenue by planThe report is built on request.
A refusal in RFC 7807 format
A
application/problem+jsonanswer is parsed by its own preset: title and detail become the message. A 403 is a «client» error — repeating it changes nothing — so the banner offers no retry, only the reason and the status badge for support.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 generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone |
| undefined | A rigid override of the tone. It beats `toneByKind`. |
texts | Partial<ResponseErrorTexts> | undefined | {} | A partial override of the texts. It is merged with `DEFAULT_RESPONSE_ERROR_TEXTS`. |
toneByKind |
| {} | The mapping of `kind` → `tone`. It is merged with `DEFAULT_TONE_BY_KIND`. |
showDetails | boolean | undefined | true | Whether to show the `details`/`fieldErrors` block under the main message. |
showFieldLabels | boolean | undefined | true | Whether to prefix `field:` before the per-field messages. |
fieldLabels | Record<string, string> | undefined | {} | Human-readable labels of the fields (`{ file: 'File' }`). |
dedupeDetails | boolean | undefined | true | Remove duplicate messages between `message` and `details`. |
canRetry | boolean | undefined | false | Whether to show the "Retry" button. |
canDismiss | boolean | undefined | true | Whether to show the "Dismiss" button. |
autoHideKinds | ResponseErrorKind[] | undefined | DEFAULT_AUTO_HIDE_KINDS | Which `kind`s not to render at all (the banner hides). |
showStatus | boolean | undefined | true | Whether to show the HTTP status badge. |
testIdPrefix | string | undefined | "response-error" | The `data-testid` prefix. |
errorrequired | ResponseErrorInfo | null | — | A ready structure of the error. If `null`, the banner is not rendered. |
Events
| Event | Signature | Description |
|---|---|---|
retry | (event: "retry", error: ResponseErrorInfo): void | — |
dismiss | (event: "dismiss"): void | — |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { UseResponseErrorOptions, UseResponseErrorReturn, ResponseErrorParserPresets } from '@feugene/granularity/components/GrResponseErrorBanner'Filedist/types/components/GrResponseErrorBanner/useResponseError.d.ts
type UseResponseErrorOptions = {
parsers?: () => ResponseErrorParser[] | undefined;type UseResponseErrorReturn = {
currentError: Ref<ResponseErrorInfo | null>;
isVisible: ComputedRef<boolean>;
lastRaw: Ref<unknown>;
setRaw: (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo | null>;function useResponseError(options?: UseResponseErrorOptions): UseResponseErrorReturn;Filedist/types/components/GrResponseErrorBanner/parsers/createResponseErrorClassifier.d.ts
const coreResponseErrorParsers: ResponseErrorParser[];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>;
messageKey?: string;
};function createResponseErrorClassifier(options?: CreateResponseErrorClassifierOptions): (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo>;
export declare function resolveResponseErrorTitle(kind: ResponseErrorKind, texts: ResponseErrorTexts): string;Filedist/types/components/GrResponseErrorBanner/parsers/normalizeError.d.ts
function normalizeError(raw: unknown): Promise<NormalizedRawError>;Filedist/types/components/GrResponseErrorBanner/responseError.types.d.ts
type ResponseErrorKind = 'network' | 'aborted' | 'validation' | 'client' | 'server' | 'unknown';type ResponseErrorTone = GrTone;type ResponseErrorFieldError = {
field: string;
messages: string[];
};type ResponseErrorInfo = {
kind: ResponseErrorKind;
status?: number;
message: string;
details?: string[];
fieldErrors?: ResponseErrorFieldError[];
raw: unknown;
meta?: Record<string, unknown>;
isFallbackMessage?: boolean;
};type NormalizedRawError = {
raw: unknown;
status?: number;
body?: unknown;
headers?: Record<string, string>;
isAbort?: boolean;
isNetwork?: boolean;
};type ResponseErrorContext = NormalizedRawError & {
meta: Record<string, unknown>;
texts: ResponseErrorTexts;
};type ResponseErrorParserResult = {
kind?: ResponseErrorKind;
status?: number;
message?: string;
details?: string[];
fieldErrors?: ResponseErrorFieldError[];
stop?: boolean;
};type ResponseErrorParser = (ctx: ResponseErrorContext) => ResponseErrorParserResult | null | undefined;
export type ResponseErrorTexts = {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;
statusLabel: string;
retryLabel: string;
dismissLabel: string;
};Filedist/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;
}Filedist/types/components/GrResponseErrorBanner/GrResponseErrorBanner.vue.d.ts
interface GrResponseErrorBannerProps {
error: ResponseErrorInfo | null;
texts?: Partial<ResponseErrorTexts>;
tone?: ResponseErrorTone;
toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
showDetails?: boolean;
showFieldLabels?: boolean;
fieldLabels?: Record<string, string>;
dedupeDetails?: boolean;
canRetry?: boolean;
canDismiss?: boolean;
autoHideKinds?: ResponseErrorKind[];
showStatus?: boolean;
testIdPrefix?: string;
}interface GrResponseErrorBannerEmits {
(e: 'retry', error: ResponseErrorInfo): void;
(e: 'dismiss'): void;
}Filedist/types/components/GrResponseErrorBanner/GrUploadErrorBanner.vue.d.ts
interface GrUploadErrorBannerProps {
error: ResponseErrorInfo | null;
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;
}Usage from the library prose
Three layers
normalizeErrorbrings axios / afetch Response/ anXMLHttpRequest/ a bareError/ a string to a common shape: the status, the parsed body, the headers, the signs of a cancellation and of a network error. The body of aResponseis read from a clone, so the consumer can read it themselves;- the chain of parsers — from the normalised context to a
ResponseErrorInfo(kind,message,details,fieldErrors). The order matters, and a parser may stop the chain (stop); - the banner — the display: the tone by
kind, the texts from the locale, the deduplication of the details.
The chain of parsers
The default is the universal core (coreResponseErrorParsers): abort, network, HTTP status, plain
message. It works at the transport level and makes no assumptions about the format of the body, so
it does not fire falsely on “almost Laravel” answers.
Server-specific parsers are connected deliberately:
const { setRaw } = useResponseError({
parsers: () => extendDefaultParsers([myParser]),
})
// or through the builder in the options of the dialogs and of the banner
errorParsers: presets => [...presets.core, presets.laravel]The ready presets: laravel, problemDetails (RFC 7807), jsonApi, fileValidation (the
client-side checking of files from GrFileUpload).
The texts, i18n and the fallback flag
The texts come from the locale (gr.responseError.*), and the texts prop goes on top. The label
of the status badge uses the same mechanism: statusLabel with a {status} insertion.
When none of the parsers produced a message, it is substituted by the classifier and marked
isFallbackMessage: true. The banner replaces only such a message with a translation.
Recognising a fallback by comparing strings, which used to be done here, threw away the server’s
answer if it happened to match the default word for word (and a server may well return
"Network error.").
Hence the contract of a parser: message is filled only with what was found in the answer. A
general text by kind is the job of the classifier, and a parser does not have to substitute it;
otherwise the message will arrive without the flag and it will no longer be possible to translate
it. Parsers that know only the type of the error (httpStatus, abort, network) do not fill the
message at all.
The message of a transport error (axios’s Request failed with status 500) does not count as a
message from the server: it is taken only if there was no answer at all. Otherwise the user would
see a technical phrase in English instead of a translated text.
The tone and accessibility
kind → the default tone: validation/client is warning, network/server/unknown is
danger, aborted is info. It is overridden pointwise (toneByKind) or as a whole (tone).
The role is inherited from GrAlert: warning and danger are announced as role="alert" (they
interrupt the speech of a screen reader), the rest as role="status" (they wait for a pause). The
banner deliberately has no setting of its own for that — GrAlert has one.
autoHideKinds hides the banner entirely for the listed kinds: the typical case is swallowing
aborted quietly.
The presets
GrFormErrorBanner and GrUploadErrorBanner are thin wrappers with preconfigurations: forms need
no retry and do need field labels, while an upload needs a retry and the context of the files (its
preset gives that into retry as { error, files } — the base banner gives away the error
itself).
They add no markup of their own and live in the folder of the base component deliberately: a separate unit of granularity would give nothing — neither CSS of its own nor a safelist of its own — and would cost two extra entries in the build. They are imported from the root of the package.
Props in this example