Enhance ApiClient with error handling and flexible HTTP methods. Introduce ApiError class for better error management, normalize base URL resolution from environment variables, and update createClient function to support custom error handling and authorization middleware.

This commit is contained in:
T. Narantuya 2025-09-19 12:57:39 +09:00
parent dcf32c1d06
commit 06009bd2d5

View File

@ -1,46 +1,83 @@
import createOpenApiClient from "openapi-fetch"; import createOpenApiClient from "openapi-fetch";
import type { paths } from "../__generated__/types"; import type { paths } from "../__generated__/types";
export class ApiError extends Error {
constructor(
message: string,
public readonly response: Response,
public readonly body?: unknown
) {
super(message);
this.name = "ApiError";
}
}
export type ApiClient = ReturnType<typeof createOpenApiClient<paths>>; export type ApiClient = ReturnType<typeof createOpenApiClient<paths>>;
export type AuthHeaderResolver = () => string | undefined; export type AuthHeaderResolver = () => string | undefined;
export interface CreateClientOptions { export interface CreateClientOptions {
getAuthHeader?: AuthHeaderResolver; getAuthHeader?: AuthHeaderResolver;
handleError?: (response: Response) => void | Promise<void>;
}
async function defaultHandleError(response: Response) {
if (response.ok) return;
let body: unknown;
let message = response.statusText || `Request failed with status ${response.status}`;
try {
const cloned = response.clone();
const contentType = cloned.headers.get("content-type");
if (contentType?.includes("application/json")) {
body = await cloned.json();
if (body && typeof body === "object" && "message" in body && typeof (body as any).message === "string") {
message = (body as any).message;
}
} else {
const text = await cloned.text();
if (text) {
body = text;
message = text;
}
}
} catch {
// Ignore body parse errors; fall back to status text
}
throw new ApiError(message, response, body);
} }
export function createClient( export function createClient(
baseUrl: string, baseUrl: string,
options: CreateClientOptions = {} options: CreateClientOptions = {}
): ApiClient { ): ApiClient {
const client = createOpenApiClient<paths>({ const client = createOpenApiClient<paths>({ baseUrl });
baseUrl,
throwOnError: true,
});
const handleError = options.handleError ?? defaultHandleError;
if (typeof client.use === "function" && options.getAuthHeader) { if (typeof client.use === "function") {
const resolveAuthHeader = options.getAuthHeader; const resolveAuthHeader = options.getAuthHeader;
client.use({ client.use({
onRequest({ request }: { request: Request }) { onRequest({ request }) {
if (!request || typeof request.headers?.has !== "function") { if (!resolveAuthHeader) return;
return; if (!request || typeof request.headers?.has !== "function") return;
} if (request.headers.has("Authorization")) return;
if (request.headers.has("Authorization")) {
return;
}
const headerValue = resolveAuthHeader(); const headerValue = resolveAuthHeader();
if (!headerValue) { if (!headerValue) return;
return;
}
request.headers.set("Authorization", headerValue); request.headers.set("Authorization", headerValue);
}, },
async onResponse({ response }) {
await handleError(response);
},
} as never); } as never);
} }
return client; return client;
} }
export type { paths };