fix(web): handle 204 No Content responses in base API service

DELETE endpoints in the automations API return 204; calling .json() on
an empty body throws SyntaxError. Treat 204 as data=null and skip
schema validation so callers can opt out of response bodies without
errors or spurious schema-mismatch warnings.

Also drops a pre-existing 'unknown → BodyInit' type error on the
non-JSON body branch via a narrow cast (caller is responsible for
passing a real BodyInit when Content-Type isn't application/json).
This commit is contained in:
CREDO23 2026-05-28 00:55:46 +02:00
parent 79f0218360
commit d48bb2033b

View file

@ -1,4 +1,5 @@
import type { ZodType } from "zod"; import type { ZodType } from "zod";
import { BACKEND_URL } from "@/lib/env-config";
import { getClientPlatform } from "../agent-filesystem"; import { getClientPlatform } from "../agent-filesystem";
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils"; import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
import { import {
@ -9,7 +10,7 @@ import {
NetworkError, NetworkError,
NotFoundError, NotFoundError,
} from "../error"; } from "../error";
import { BACKEND_URL } from "@/lib/env-config";
enum ResponseType { enum ResponseType {
JSON = "json", JSON = "json",
TEXT = "text", TEXT = "text",
@ -122,8 +123,9 @@ class BaseApiService {
if (contentType === "application/json" && typeof mergedOptions.body === "object") { if (contentType === "application/json" && typeof mergedOptions.body === "object") {
fetchOptions.body = JSON.stringify(mergedOptions.body); fetchOptions.body = JSON.stringify(mergedOptions.body);
} else { } else {
// Pass body as-is for other content types (e.g., form data, already stringified) // Pass body as-is for other content types (form data, already stringified).
fetchOptions.body = mergedOptions.body; // Caller is responsible for passing a real BodyInit when Content-Type is not JSON.
fetchOptions.body = mergedOptions.body as BodyInit;
} }
} }
@ -210,32 +212,39 @@ class BaseApiService {
let data; let data;
const responseType = mergedOptions.responseType; const responseType = mergedOptions.responseType;
try { if (response.status === 204) {
switch (responseType) { // 204 No Content has no body; .json() would throw SyntaxError.
case ResponseType.JSON: // Leave data as null and skip schema validation below so endpoints
data = await response.json(); // that opt out of bodies (REST-style DELETE) don't error on success.
break; data = null;
case ResponseType.TEXT: } else {
data = await response.text(); try {
break; switch (responseType) {
case ResponseType.BLOB: case ResponseType.JSON:
data = await response.blob(); data = await response.json();
break; break;
case ResponseType.ARRAY_BUFFER: case ResponseType.TEXT:
data = await response.arrayBuffer(); data = await response.text();
break; break;
// Add more cases as needed case ResponseType.BLOB:
default: data = await response.blob();
data = await response.json(); break;
case ResponseType.ARRAY_BUFFER:
data = await response.arrayBuffer();
break;
// Add more cases as needed
default:
data = await response.json();
}
} catch (error) {
console.error("Failed to parse response as JSON:", error);
throw new AppError("Failed to parse response", response.status, response.statusText);
} }
} catch (error) {
console.error("Failed to parse response as JSON:", error);
throw new AppError("Failed to parse response", response.status, response.statusText);
} }
// Validate response // Validate response
if (responseType === ResponseType.JSON) { if (responseType === ResponseType.JSON) {
if (!responseSchema) { if (!responseSchema || response.status === 204) {
return data; return data;
} }
const parsedData = responseSchema.safeParse(data); const parsedData = responseSchema.safeParse(data);