fix: handle 4XX and 204 properly (#8722)

Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Alejandro Bailo
2025-09-15 17:07:15 +02:00
committed by GitHub
parent ec27451199
commit 940a1202b3
11 changed files with 79 additions and 23 deletions
+3
View File
@@ -78,3 +78,6 @@ _data/
# Claude
CLAUDE.md
# LLM's (Until we have a standard one)
AGENTS.md
+8 -1
View File
@@ -2,9 +2,10 @@
All notable changes to the **Prowler UI** are documented in this file.
## [1.13.0] (Prowler unreleased)
## [1.13.0] (Prowler UNRELEASED)
### 🚀 Added
- Support for Markdown and AdditionalURLs in findings detail page [(#8704)](https://github.com/prowler-cloud/prowler/pull/8704)
- `Prowler Hub` menu item with tooltip [(#8692)] (https://github.com/prowler-cloud/prowler/pull/8692)
- Copy link button to finding detail page [(#8685)] (https://github.com/prowler-cloud/prowler/pull/8685)
@@ -15,6 +16,12 @@ 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
+1 -2
View File
@@ -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: {},
+2 -1
View File
@@ -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;
@@ -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",
@@ -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",
@@ -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",
@@ -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<typeof addRoleFormSchema>;
@@ -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),
});
}
};
@@ -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<typeof editRoleFormSchema>;
@@ -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),
});
}
};
+2 -1
View File
@@ -19,7 +19,8 @@ export const useFormServerErrors = <T extends Record<string, any>>(
) => {
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, {
+49 -9
View File
@@ -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);