diff --git a/.gitignore b/.gitignore index 6e7e284566..c6febde53c 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,6 @@ _data/ # Claude CLAUDE.md + +# LLM's (Until we have a standard one) +AGENTS.md diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 054e4334fa..807f80dc3c 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the **Prowler UI** are documented in this file. + +## [1.12.2] (Prowler v5.12.2) + +### 🐞 Fixed + +- Handle 4XX errors consistently and 204 responses properly[(#8722)](https://github.com/prowler-cloud/prowler/pull/8722) + ## [1.12.1] (Prowler v5.12.1) ### 🐞 Fixed diff --git a/ui/actions/roles/roles.ts b/ui/actions/roles/roles.ts index 36295fe462..ea43aa21bb 100644 --- a/ui/actions/roles/roles.ts +++ b/ui/actions/roles/roles.ts @@ -159,8 +159,7 @@ export const updateRole = async (formData: FormData, roleId: string) => { manage_providers: formData.get("manage_providers") === "true", manage_account: formData.get("manage_account") === "true", manage_scans: formData.get("manage_scans") === "true", - // TODO: Add back when we have integrations ready - // manage_integrations: formData.get("manage_integrations") === "true", + manage_integrations: formData.get("manage_integrations") === "true", unlimited_visibility: formData.get("unlimited_visibility") === "true", }, relationships: {}, diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx index 7e11d7cf9c..6faf5d99ed 100644 --- a/ui/components/auth/oss/auth-form.tsx +++ b/ui/components/auth/oss/auth-form.tsx @@ -147,7 +147,8 @@ export const AuthForm = ({ } else { newUser.errors.forEach((error: ApiError) => { const errorMessage = error.detail; - switch (error.source.pointer) { + const pointer = error.source?.pointer; + switch (pointer) { case "/data/attributes/name": form.setError("name", { type: "server", message: errorMessage }); break; diff --git a/ui/components/invitations/workflow/forms/send-invitation-form.tsx b/ui/components/invitations/workflow/forms/send-invitation-form.tsx index 4a18643724..73b13e5760 100644 --- a/ui/components/invitations/workflow/forms/send-invitation-form.tsx +++ b/ui/components/invitations/workflow/forms/send-invitation-form.tsx @@ -53,7 +53,8 @@ export const SendInvitationForm = ({ if (data?.errors && data.errors.length > 0) { data.errors.forEach((error: ApiError) => { const errorMessage = error.detail; - switch (error.source.pointer) { + const pointer = error.source?.pointer; + switch (pointer) { case "/data/attributes/email": form.setError("email", { type: "server", diff --git a/ui/components/manage-groups/forms/add-group-form.tsx b/ui/components/manage-groups/forms/add-group-form.tsx index 45cb5e6b06..54dfedc9f4 100644 --- a/ui/components/manage-groups/forms/add-group-form.tsx +++ b/ui/components/manage-groups/forms/add-group-form.tsx @@ -69,7 +69,8 @@ export const AddGroupForm = ({ if (data?.errors && data.errors.length > 0) { data.errors.forEach((error: ApiError) => { const errorMessage = error.detail; - switch (error.source.pointer) { + const pointer = error.source?.pointer; + switch (pointer) { case "/data/attributes/name": form.setError("name", { type: "server", diff --git a/ui/components/manage-groups/forms/edit-group-form.tsx b/ui/components/manage-groups/forms/edit-group-form.tsx index 4a58622d2c..f225ead2f4 100644 --- a/ui/components/manage-groups/forms/edit-group-form.tsx +++ b/ui/components/manage-groups/forms/edit-group-form.tsx @@ -105,7 +105,8 @@ export const EditGroupForm = ({ if (data?.errors && data.errors.length > 0) { data.errors.forEach((error: ApiError) => { const errorMessage = error.detail; - switch (error.source.pointer) { + const pointer = error.source?.pointer; + switch (pointer) { case "/data/attributes/name": form.setError("name", { type: "server", diff --git a/ui/components/roles/workflow/forms/add-role-form.tsx b/ui/components/roles/workflow/forms/add-role-form.tsx index a833cbe2c2..7cc0d7df08 100644 --- a/ui/components/roles/workflow/forms/add-role-form.tsx +++ b/ui/components/roles/workflow/forms/add-role-form.tsx @@ -17,7 +17,7 @@ import { CustomInput, } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; -import { permissionFormFields } from "@/lib"; +import { getErrorMessage, permissionFormFields } from "@/lib"; import { addRoleFormSchema, ApiError } from "@/types"; type FormValues = z.infer; @@ -113,7 +113,8 @@ export const AddRoleForm = ({ if (data?.errors && data.errors.length > 0) { data.errors.forEach((error: ApiError) => { const errorMessage = error.detail; - switch (error.source.pointer) { + const pointer = error.source?.pointer; + switch (pointer) { case "/data/attributes/name": form.setError("name", { type: "server", @@ -139,7 +140,7 @@ export const AddRoleForm = ({ toast({ variant: "destructive", title: "Error", - description: "An unexpected error occurred. Please try again.", + description: getErrorMessage(error), }); } }; diff --git a/ui/components/roles/workflow/forms/edit-role-form.tsx b/ui/components/roles/workflow/forms/edit-role-form.tsx index 7573d40502..b8783abc61 100644 --- a/ui/components/roles/workflow/forms/edit-role-form.tsx +++ b/ui/components/roles/workflow/forms/edit-role-form.tsx @@ -17,7 +17,7 @@ import { CustomInput, } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; -import { permissionFormFields } from "@/lib"; +import { getErrorMessage, permissionFormFields } from "@/lib"; import { ApiError, editRoleFormSchema } from "@/types"; type FormValues = z.infer; @@ -133,7 +133,8 @@ export const EditRoleForm = ({ if (data?.errors && data.errors.length > 0) { data.errors.forEach((error: ApiError) => { const errorMessage = error.detail; - switch (error.source.pointer) { + const pointer = error.source?.pointer; + switch (pointer) { case "/data/attributes/name": form.setError("name", { type: "server", @@ -159,7 +160,7 @@ export const EditRoleForm = ({ toast({ variant: "destructive", title: "Error", - description: "An unexpected error occurred. Please try again.", + description: getErrorMessage(error), }); } }; diff --git a/ui/hooks/use-form-server-errors.ts b/ui/hooks/use-form-server-errors.ts index d8d662bac7..84337c42e6 100644 --- a/ui/hooks/use-form-server-errors.ts +++ b/ui/hooks/use-form-server-errors.ts @@ -19,7 +19,8 @@ export const useFormServerErrors = >( ) => { errors.forEach((error: ApiError) => { const errorMessage = error.detail; - const fieldName = errorMapping?.[error.source.pointer]; + const pointer = error.source?.pointer; + const fieldName = pointer ? errorMapping?.[pointer] : undefined; if (fieldName && fieldName in form.formState.defaultValues!) { form.setError(fieldName as any, { diff --git a/ui/lib/helper.ts b/ui/lib/helper.ts index 016201c5bd..85e5ca0e5b 100644 --- a/ui/lib/helper.ts +++ b/ui/lib/helper.ts @@ -348,10 +348,27 @@ export const handleApiResponse = async ( parse = true, ) => { if (!response.ok) { - const errorData = await response.json().catch(() => null); - const errorDetail = errorData?.errors?.[0]?.detail; + // Read error body safely; prefer JSON, fallback to plain text + const rawErrorText = await response.text().catch(() => ""); + let errorData: any = null; + try { + errorData = rawErrorText ? JSON.parse(rawErrorText) : null; + } catch { + errorData = null; + } - // Special handling for server errors (500+) + const errorsArray = Array.isArray(errorData?.errors) + ? (errorData.errors as any[]) + : undefined; + const errorDetail = + errorsArray?.[0]?.detail || + errorData?.error || + errorData?.message || + (rawErrorText && rawErrorText.trim()) || + response.statusText || + "Oops! Something went wrong."; + + //5XX errors if (response.status >= 500) { throw new Error( errorDetail || @@ -359,14 +376,37 @@ export const handleApiResponse = async ( ); } - // Client errors (4xx) - throw new Error( - errorDetail || - `Request failed (${response.status}): ${response.statusText}`, - ); + return errorsArray + ? { error: errorDetail, errors: errorsArray, status: response.status } + : ({ error: errorDetail, status: response.status } as any); } - const data = await response.json(); + // Handle empty or no-content responses gracefully (e.g., 204, empty body) + if (response.status === 204) { + if (pathToRevalidate && pathToRevalidate !== "") { + revalidatePath(pathToRevalidate); + } + return { success: true, status: response.status } as any; + } + + // Read raw text to determine if there's a body to parse + const rawText = await response.text(); + const hasBody = rawText && rawText.trim().length > 0; + + if (!hasBody) { + if (pathToRevalidate && pathToRevalidate !== "") { + revalidatePath(pathToRevalidate); + } + return { success: true, status: response.status } as any; + } + + let data: any; + try { + data = JSON.parse(rawText); + } catch (e) { + // If body isn't valid JSON, return as text payload + data = { data: rawText }; + } if (pathToRevalidate && pathToRevalidate !== "") { revalidatePath(pathToRevalidate);