码桶
发现社区成员的开源项目
api.js6.1 KB
/**
* Centralized API client for cf-dns-manager.
*
* Provides a thin fetch wrapper that:
* - Auto-attaches auth headers from the current auth state
* - Auto-sets Content-Type: application/json
* - On 401 response, attempts one token refresh then retries
* - Parses JSON responses automatically
* - Throws typed ApiError for error responses
* - Supports abort signals
*/
export class ApiError extends Error {
/**
* @param {number} status - HTTP status code
* @param {string} message - Human-readable error message
* @param {*} [data] - Optional parsed response body
*/
constructor(status, message, data) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}
export class ApiClient {
/**
* @param {() => import('../types').AuthState | null} getAuth
* Returns the current auth state.
* @param {((auth: import('../types').AuthState) => Promise<import('../types').AuthState | null>) | null} onRefreshToken
* Called when a 401 is received. Should attempt a token refresh and
* return the updated auth state, or null on failure.
*/
constructor(getAuth, onRefreshToken = null) {
this._getAuth = getAuth;
this._onRefreshToken = onRefreshToken;
}
// ------------------------------------------------------------------ //
// Convenience verbs //
// ------------------------------------------------------------------ //
/**
* @param {string} url
* @param {RequestInit} [options]
*/
async get(url, options) {
return this.request(url, { ...options, method: 'GET' });
}
/**
* @param {string} url
* @param {*} body
* @param {RequestInit} [options]
*/
async post(url, body, options) {
return this.request(url, { ...options, method: 'POST', body: JSON.stringify(body) });
}
/**
* @param {string} url
* @param {*} body
* @param {RequestInit} [options]
*/
async put(url, body, options) {
return this.request(url, { ...options, method: 'PUT', body: JSON.stringify(body) });
}
/**
* @param {string} url
* @param {*} body
* @param {RequestInit} [options]
*/
async patch(url, body, options) {
return this.request(url, { ...options, method: 'PATCH', body: JSON.stringify(body) });
}
/**
* @param {string} url
* @param {RequestInit} [options]
*/
async del(url, options) {
return this.request(url, { ...options, method: 'DELETE' });
}
// ------------------------------------------------------------------ //
// Core request method //
// ------------------------------------------------------------------ //
/**
* @param {string} url
* @param {RequestInit} [options]
* @returns {Promise<*>} Parsed JSON response body.
*/
async request(url, options = {}) {
const doFetch = async (authOverride) => {
const auth = authOverride || this._getAuth();
const headers = new Headers(options.headers || {});
// Auto-set Content-Type when not already present and there is a body
if (!headers.has('Content-Type') && options.body != null) {
headers.set('Content-Type', 'application/json');
}
// Auto-attach auth headers
if (auth) {
if (auth._localToken) {
headers.set('X-Cloudflare-Token', auth._localToken);
} else if (auth.mode === 'server') {
headers.set('Authorization', `Bearer ${auth.token}`);
headers.set('X-Managed-Account-Index', String(auth.currentAccountIndex || 0));
} else {
headers.set('X-Cloudflare-Token', auth.token);
}
}
const fetchOptions = {
...options,
headers,
};
// Forward abort signal if provided
if (options.signal) {
fetchOptions.signal = options.signal;
}
return fetch(url, fetchOptions);
};
// First attempt
let res = await doFetch();
// On 401, try refreshing the token once then retry
if (res.status === 401 && this._onRefreshToken) {
const auth = this._getAuth();
if (auth) {
const refreshed = await this._onRefreshToken(auth);
if (refreshed) {
res = await doFetch(refreshed);
}
}
}
// Parse response body
let data;
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
data = await res.json();
} else {
// Attempt JSON parse; fall back to text
const text = await res.text();
try {
data = JSON.parse(text);
} catch {
data = text;
}
}
if (!res.ok) {
const message =
(data && (data.error || data.message)) ||
`Request failed with status ${res.status}`;
throw new ApiError(res.status, message, data);
}
return data;
}
}
/**
* Creates an ApiClient instance wired to the given auth accessors.
*
* Usage (inside a React component / hook):
*
* const apiRef = useRef(null);
* if (!apiRef.current) {
* apiRef.current = createApiClient(() => authRef.current, tryRefresh);
* }
* const api = apiRef.current;
* const data = await api.get('/api/zones');
*
* @param {() => import('../types').AuthState | null} getAuth
* @param {((auth: import('../types').AuthState) => Promise<import('../types').AuthState | null>) | null} onRefreshToken
* @returns {ApiClient}
*/
export function createApiClient(getAuth, onRefreshToken = null) {
return new ApiClient(getAuth, onRefreshToken);
}