fetch-api-client
Version:
A TypeScript API client using fetch with axios-like interface
312 lines (311 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiClient = void 0;
exports.createClient = createClient;
const interceptors_1 = require("./interceptors");
/**
* Type guard functions remain the same...
*/
function isFetchTypeError(error) {
return error instanceof TypeError && typeof error.message === 'string';
}
function isAbortError(error) {
return error instanceof Error && error.name === 'AbortError';
}
function isApiError(error) {
return typeof error === 'object' &&
error !== null &&
'status' in error &&
'message' in error;
}
/**
* Main API Client class that provides axios-like interface using fetch
*/
class ApiClient {
constructor(config = {}) {
// Set default configuration with proper credentials
this.config = {
baseURL: '',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
getToken: () => null,
credentials: 'include', // ✅ Always include cookies by default
validateStatus: (status) => status >= 200 && status < 300,
...config,
};
// Initialize interceptor managers
this.interceptors = {
request: new interceptors_1.RequestInterceptorManager(),
response: new interceptors_1.ResponseInterceptorManager(),
};
}
/**
* Build complete URL from base URL and endpoint
*/
buildUrl(url, params) {
const fullUrl = url.startsWith('http') ? url : `${this.config.baseURL}${url}`;
if (!params)
return fullUrl;
const urlObj = new URL(fullUrl);
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
urlObj.searchParams.append(key, String(value));
}
});
return urlObj.toString();
}
/**
* Prepare headers for the request
*/
async prepareHeaders(config) {
const headers = new Headers();
// Add default headers
Object.entries(this.config.headers).forEach(([key, value]) => {
headers.set(key, value);
});
// Add request-specific headers
if (config.headers) {
Object.entries(config.headers).forEach(([key, value]) => {
headers.set(key, value);
});
}
// Add authorization token if available
try {
const token = await this.config.getToken();
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
}
catch (error) {
console.warn('Failed to get token:', error);
}
return headers;
}
/**
* Prepare request body
*/
prepareBody(data, headers) {
if (!data)
return null;
const contentType = headers.get('Content-Type');
if (data instanceof FormData) {
// Remove Content-Type header for FormData to let browser set it
headers.delete('Content-Type');
return data;
}
if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/json')) {
return JSON.stringify(data);
}
if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/x-www-form-urlencoded')) {
const params = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
params.append(key, String(value));
});
return params.toString();
}
return String(data);
}
/**
* Create AbortController with timeout
*/
createAbortController(timeout) {
const controller = new AbortController();
const timeoutMs = timeout !== null && timeout !== void 0 ? timeout : this.config.timeout;
if (timeoutMs > 0) {
setTimeout(() => {
controller.abort();
}, timeoutMs);
}
return controller;
}
/**
* Parse response based on content type
*/
async parseResponse(response) {
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
if (contentType.includes('text/')) {
return response.text();
}
if (contentType.includes('application/octet-stream') || contentType.includes('image/')) {
return response.blob();
}
return response.text();
}
/**
* Create standardized error object
*/
createError(message, status, statusText, data, config) {
return {
message,
status,
statusText,
data,
config,
code: status ? `HTTP_${status}` : 'NETWORK_ERROR',
};
}
/**
* Core request method - Enhanced with CORS-friendly credentials handling
*/
async request(config) {
var _a;
try {
// Process request through interceptors
const processedConfig = await this.interceptors.request.execute(config);
// Prepare request components
const url = this.buildUrl(processedConfig.url || '', processedConfig.params);
const headers = await this.prepareHeaders(processedConfig);
const body = this.prepareBody(processedConfig.data, headers);
const controller = processedConfig.signal ?
{ signal: processedConfig.signal } :
this.createAbortController(processedConfig.timeout);
// ✅ Use proper credentials configuration - avoid 'include' for CORS-sensitive requests
const credentials = (_a = processedConfig.credentials) !== null && _a !== void 0 ? _a : this.config.credentials;
// ✅ Clean headers to avoid CORS preflight triggers
const cleanHeaders = new Headers();
headers.forEach((value, key) => {
// Only include CORS-safe headers
const corsUnsafeHeaders = [
'x-ios-device',
'x-ios-version',
'x-platform',
'x-device-type',
'pragma',
'cache-control'
];
if (!corsUnsafeHeaders.includes(key.toLowerCase())) {
cleanHeaders.set(key, value);
}
});
// Execute fetch request
const response = await fetch(url, {
method: processedConfig.method || 'GET',
headers: cleanHeaders, // ✅ Use cleaned headers
body,
credentials, // ✅ Use proper fetch credentials API
...controller,
});
// Parse response data
const data = await this.parseResponse(response);
// Check if response status is valid
if (!this.config.validateStatus(response.status)) {
const error = this.createError(`Request failed with status ${response.status}`, response.status, response.statusText, data, processedConfig);
const processedError = await this.interceptors.response.executeRejected(error);
throw processedError;
}
// Create successful response
const apiResponse = {
data,
status: response.status,
statusText: response.statusText,
headers: response.headers,
config: processedConfig,
};
// Process response through interceptors
return await this.interceptors.response.executeFulfilled(apiResponse);
}
catch (error) {
// Handle different types of errors with proper type checking
if (isFetchTypeError(error) && error.message.includes('fetch')) {
const networkError = this.createError('Network Error', undefined, undefined, undefined, config);
const processedError = await this.interceptors.response.executeRejected(networkError);
throw processedError;
}
if (isAbortError(error)) {
const timeoutError = this.createError('Request Timeout', undefined, undefined, undefined, config);
const processedError = await this.interceptors.response.executeRejected(timeoutError);
throw processedError;
}
// Re-throw if it's already our custom error
if (isApiError(error)) {
throw error;
}
// Create generic error for unknown error types
const errorMessage = error instanceof Error ? error.message : 'Unknown Error';
const genericError = this.createError(errorMessage, undefined, undefined, undefined, config);
const processedError = await this.interceptors.response.executeRejected(genericError);
throw processedError;
}
}
/**
* HTTP methods remain the same but ensure credentials are properly passed
*/
async get(url, config) {
var _a;
return this.request({
...config,
method: 'GET',
url,
credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included
});
}
async post(url, data, config) {
var _a;
return this.request({
...config,
method: 'POST',
url,
data,
credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included
});
}
async put(url, data, config) {
var _a;
return this.request({
...config,
method: 'PUT',
url,
data,
credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included
});
}
async delete(url, config) {
var _a;
return this.request({
...config,
method: 'DELETE',
url,
credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included
});
}
async patch(url, data, config) {
var _a;
return this.request({
...config,
method: 'PATCH',
url,
data,
credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included
});
}
async head(url, config) {
var _a;
return this.request({
...config,
method: 'HEAD',
url,
credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included
});
}
async options(url, config) {
var _a;
return this.request({
...config,
method: 'OPTIONS',
url,
credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included
});
}
}
exports.ApiClient = ApiClient;
/**
* Create a new API client instance
*/
function createClient(config) {
return new ApiClient(config);
}