Files
OmniRoute/src/shared/utils/api.ts
Diego Rodrigues de Sa e Souza d0396c200d Release v3.8.31 (#4377)
Release v3.8.31 — see CHANGELOG.md [3.8.31] for full notes and contributors.

Merged over known non-blocking reds (all correctness gates green): Integration Tests (2/2) is env/flaky (polls a real upstream batch that did not complete in the poll window); SonarQube/SonarCloud is the advisory server-side new-code quality gate. Unit (8 shards), Coverage, Node 22/24/26, Lint, PR Test Policy, Quality Ratchet, Docs-Strict, Quality-Extended and all 4 CodeQL analyses are green.
2026-06-20 14:55:24 -03:00

113 lines
3.3 KiB
TypeScript

/**
* API utility functions for making HTTP requests
*/
const DEFAULT_HEADERS: Record<string, string> = {
"Content-Type": "application/json",
};
interface ApiOptions extends RequestInit {
headers?: Record<string, string>;
}
export async function get(url: string, options: ApiOptions = {}) {
const response = await fetch(url, {
...options,
method: "GET",
headers: { ...DEFAULT_HEADERS, ...options.headers },
});
return handleResponse(response);
}
export async function post(url: string, data: unknown, options: ApiOptions = {}) {
const response = await fetch(url, {
...options,
method: "POST",
headers: { ...DEFAULT_HEADERS, ...options.headers },
body: JSON.stringify(data),
});
return handleResponse(response);
}
export async function put(url: string, data: unknown, options: ApiOptions = {}) {
const response = await fetch(url, {
...options,
method: "PUT",
headers: { ...DEFAULT_HEADERS, ...options.headers },
body: JSON.stringify(data),
});
return handleResponse(response);
}
export async function del(url: string, options: ApiOptions = {}) {
const response = await fetch(url, {
...options,
method: "DELETE",
headers: { ...DEFAULT_HEADERS, ...options.headers },
});
return handleResponse(response);
}
/**
* Safely read a fetch `Response` body. Returns the parsed JSON when the body is
* JSON, the raw text when it is not, or `null` when empty. Never throws on a
* non-JSON body — a plain-text `500 Internal Server Error` no longer surfaces to
* the caller as `Unexpected token 'I'…`. (#1318) The body is read exactly once,
* so pass the returned value to {@link getErrorMessage} rather than re-reading
* the response.
*/
export async function parseResponseBody(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return text;
}
}
/**
* Extract a human-readable error message from an already-parsed response body
* (see {@link parseResponseBody}), regardless of whether it is a JSON object
* (`{ error }` / `{ error: { message } }` / `{ message }`) or plain text. Falls
* back to a status-qualified default. (#1318)
*/
export function getErrorMessage(
body: unknown,
status?: number,
fallback = "Request failed"
): string {
if (body && typeof body === "object") {
const rec = body as Record<string, unknown>;
const err = rec.error;
if (typeof err === "string" && err.trim()) return err;
if (err && typeof err === "object") {
const message = (err as Record<string, unknown>).message;
if (typeof message === "string" && message.trim()) return message;
return JSON.stringify(err);
}
const msg = rec.message ?? rec.detail;
if (typeof msg === "string" && msg.trim()) return msg;
}
if (typeof body === "string" && body.trim()) {
return body.length > 300 ? `${body.slice(0, 300)}` : body;
}
return status != null ? `${fallback} (HTTP ${status})` : fallback;
}
async function handleResponse(response: Response) {
const data = await response.json();
if (!response.ok) {
const error: any = new Error(data.error || "An error occurred");
error.status = response.status;
error.data = data;
throw error;
}
return data;
}
const api = { get, post, put, del };
export default api;