web/admin: improve error handling (cherry-pick #11212) (#11219)

web/admin: improve error handling (#11212)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
This commit is contained in:
gcp-cherry-pick-bot[bot]
2024-09-05 13:48:28 +02:00
committed by GitHub
parent 905800e535
commit e77480ee1d
5 changed files with 33 additions and 34 deletions

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIError } from "@goauthentik/common/errors";
import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
@ -24,7 +25,6 @@ import {
type TransactionApplicationRequest, type TransactionApplicationRequest,
type TransactionApplicationResponse, type TransactionApplicationResponse,
ValidationError, ValidationError,
ValidationErrorFromJSON,
} from "@goauthentik/api"; } from "@goauthentik/api";
import BasePanel from "../BasePanel"; import BasePanel from "../BasePanel";
@ -133,9 +133,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => { .catch(async (resolution: any) => {
const errors = (this.errors = ValidationErrorFromJSON( const errors = await parseAPIError(resolution);
await resolution.response.json(),
));
this.dispatchWizardUpdate({ this.dispatchWizardUpdate({
update: { update: {
...this.wizard, ...this.wizard,

View File

@ -1,4 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { parseAPIError } from "@goauthentik/common/errors";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -22,7 +23,7 @@ import {
PromptTypeEnum, PromptTypeEnum,
ResponseError, ResponseError,
StagesApi, StagesApi,
ValidationErrorFromJSON, ValidationError,
} from "@goauthentik/api"; } from "@goauthentik/api";
class PreviewStageHost implements StageHost { class PreviewStageHost implements StageHost {
@ -83,10 +84,8 @@ export class PromptForm extends ModelForm<Prompt, string> {
}); });
this.previewError = undefined; this.previewError = undefined;
} catch (exc) { } catch (exc) {
const errorMessage = ValidationErrorFromJSON( const errorMessage = parseAPIError(exc as ResponseError);
await (exc as ResponseError).response.json(), this.previewError = (errorMessage as ValidationError).nonFieldErrors;
);
this.previewError = errorMessage.nonFieldErrors;
} }
} }

View File

@ -12,11 +12,17 @@ export class RequestError extends Error {}
export type APIErrorTypes = ValidationError | GenericError; export type APIErrorTypes = ValidationError | GenericError;
export const HTTP_BAD_REQUEST = 400;
export const HTTP_INTERNAL_SERVICE_ERROR = 500;
export async function parseAPIError(error: Error): Promise<APIErrorTypes> { export async function parseAPIError(error: Error): Promise<APIErrorTypes> {
if (!(error instanceof ResponseError)) { if (!(error instanceof ResponseError)) {
return error; return error;
} }
if (error.response.status < 400 || error.response.status > 499) { if (
error.response.status < HTTP_BAD_REQUEST ||
error.response.status >= HTTP_INTERNAL_SERVICE_ERROR
) {
return error; return error;
} }
const body = await error.response.json(); const body = await error.response.json();

View File

@ -1,4 +1,5 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIError } from "@goauthentik/common/errors";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils"; import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -6,6 +7,7 @@ import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFor
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers"; import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { property, state } from "lit/decorators.js"; import { property, state } from "lit/decorators.js";
@ -18,7 +20,7 @@ import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-gro
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css"; import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ResponseError, ValidationError, ValidationErrorFromJSON } from "@goauthentik/api"; import { ResponseError, ValidationError, instanceOfValidationError } from "@goauthentik/api";
export class APIError extends Error { export class APIError extends Error {
constructor(public response: ValidationError) { constructor(public response: ValidationError) {
@ -124,9 +126,6 @@ export function serializeForm<T extends KeyUnknown>(
return json as unknown as T; return json as unknown as T;
} }
const HTTP_BAD_REQUEST = 400;
const HTTP_INTERNAL_SERVICE_ERROR = 500;
/** /**
* Form * Form
* *
@ -307,18 +306,9 @@ export abstract class Form<T> extends AKElement {
return response; return response;
} catch (ex) { } catch (ex) {
if (ex instanceof ResponseError) { if (ex instanceof ResponseError) {
let msg = ex.response.statusText; let errorMessage = ex.response.statusText;
if ( const error = await parseAPIError(ex);
ex.response.status >= HTTP_BAD_REQUEST && if (instanceOfValidationError(error)) {
ex.response.status < HTTP_INTERNAL_SERVICE_ERROR
) {
const errorMessage = ValidationErrorFromJSON(await ex.response.json());
if (!errorMessage) {
return errorMessage;
}
if (errorMessage instanceof Error) {
throw errorMessage;
}
// assign all input-related errors to their elements // assign all input-related errors to their elements
const elements = const elements =
this.shadowRoot?.querySelectorAll<HorizontalFormElement>( this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
@ -330,26 +320,28 @@ export abstract class Form<T> extends AKElement {
if (!elementName) { if (!elementName) {
return; return;
} }
if (camelToSnake(elementName) in errorMessage) { if (camelToSnake(elementName) in error) {
element.errorMessages = errorMessage[camelToSnake(elementName)]; element.errorMessages = (error as ValidationError)[
camelToSnake(elementName)
];
element.invalid = true; element.invalid = true;
} else { } else {
element.errorMessages = []; element.errorMessages = [];
element.invalid = false; element.invalid = false;
} }
}); });
if (errorMessage.nonFieldErrors) { if ((error as ValidationError).nonFieldErrors) {
this.nonFieldErrors = errorMessage.nonFieldErrors; this.nonFieldErrors = (error as ValidationError).nonFieldErrors;
} }
errorMessage = msg("Invalid update request.");
// Only change the message when we have `detail`. // Only change the message when we have `detail`.
// Everything else is handled in the form. // Everything else is handled in the form.
if ("detail" in errorMessage) { if ("detail" in (error as ValidationError)) {
msg = errorMessage.detail; errorMessage = (error as ValidationError).detail;
} }
} }
// error is local or not from rest_framework
showMessage({ showMessage({
message: msg, message: errorMessage,
level: MessageLevel.error, level: MessageLevel.error,
}); });
} }

View File

@ -9,6 +9,7 @@ import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/messages/Message"; import "@goauthentik/elements/messages/Message";
import { APIMessage } from "@goauthentik/elements/messages/Message"; import { APIMessage } from "@goauthentik/elements/messages/Message";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
@ -20,6 +21,9 @@ export function showMessage(message: APIMessage, unique = false): void {
if (!container) { if (!container) {
throw new SentryIgnoredError("failed to find message container"); throw new SentryIgnoredError("failed to find message container");
} }
if (message.message.trim() === "") {
message.message = msg("Error");
}
container.addMessage(message, unique); container.addMessage(message, unique);
container.requestUpdate(); container.requestUpdate();
} }