chore: standardize security and quality foundations
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { authFetchOrThrow, getApiBase } from "./auth";
|
||||
|
||||
export type ApiTransport = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
export class ApiClientError extends Error {
|
||||
readonly status: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.name = "ApiClientError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = (payload: unknown, fallback: string) => {
|
||||
if (!payload || typeof payload !== "object") return fallback;
|
||||
const record = payload as Record<string, unknown>;
|
||||
for (const key of ["detail", "error", "message"]) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const apiUrl = (path: string) => {
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${getApiBase()}${normalizedPath}`;
|
||||
};
|
||||
|
||||
export async function requestJson<T>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
transport: ApiTransport = authFetchOrThrow,
|
||||
): Promise<T> {
|
||||
const response = await transport(apiUrl(path), init);
|
||||
if (response.status === 204) return undefined as T;
|
||||
|
||||
const text = await response.text();
|
||||
let payload: unknown = null;
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch {
|
||||
payload = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiClientError(response.status, errorMessage(payload, text || `Request failed: ${response.status}`));
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
Reference in New Issue
Block a user