Files
server/core/src/utils/helpers/fetch-wrapper-core.ts
T
Sebastian 6931dc9ff7 fix: expired token issue
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-05 11:36:20 -04:00

104 lines
3.3 KiB
TypeScript

/**
* Core fetch wrapper - reusable across modules
*/
export interface FetchWrapperOptions {
/** Additional headers merged into every request */
headers?: Record<string, string>;
/** Override default Content-Type (default: application/json) */
contentType?: string;
/** Override default Accept type (default: application/json) */
accept?: string;
/** Called before every request */
beforeRequest?: () => Promise<void>;
/** Called after every successful non-stream response */
afterResponse?: (data: any) => void;
}
export interface RequestCallOptions {
/** When set, the raw Response is forwarded to this handler instead of being JSON-parsed */
onStream?: (response: Response) => Promise<void>;
/** Per-call header overrides — highest priority, wins over factory-level headers */
headers?: Record<string, string>;
}
/** Error thrown for non-2xx responses, carrying the HTTP status code */
export class FetchError extends Error {
constructor(message: string, public readonly status: number) {
super(message);
this.name = 'FetchError';
}
}
function getCsrfToken(): string | null {
if (typeof document === 'undefined') return null;
const cookie = document.cookie.split('; ').find(r => r.startsWith('X-CSRF-TOKEN='));
return cookie ? (cookie.split('=')[1] ?? null) : null;
}
export function createFetchWrapper(options: FetchWrapperOptions = {}) {
async function request(
method: string,
url: string,
body?: object,
callOptions?: RequestCallOptions
): Promise<any> {
if (options.beforeRequest) {
await options.beforeRequest();
}
// Header priority: defaults < factory options.headers < callOptions.headers
const headers: Record<string, string> = {
'Content-Type': options.contentType ?? 'application/json',
'Accept': options.accept ?? 'application/json',
...options.headers,
...callOptions?.headers,
};
const csrf = getCsrfToken();
if (csrf) headers['X-CSRF-TOKEN'] = csrf;
const requestOptions: RequestInit = {
method,
headers,
credentials: 'include',
body: body ? JSON.stringify(body) : undefined,
};
const response = await fetch(url, requestOptions);
if (!response.ok) {
const text = await response.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch { /* non-JSON error body */ }
throw new FetchError(data?.message || response.statusText, response.status);
}
if (callOptions?.onStream) {
return callOptions.onStream(response);
}
const text = await response.text();
const data = text ? JSON.parse(text) : null;
options.afterResponse?.(data);
return data;
}
return {
get: (url: string, callOptions?: RequestCallOptions) =>
request('GET', url, undefined, callOptions),
post: (url: string, body?: object, callOptions?: RequestCallOptions) =>
request('POST', url, body, callOptions),
put: (url: string, body?: object, callOptions?: RequestCallOptions) =>
request('PUT', url, body, callOptions),
patch: (url: string, body?: object, callOptions?: RequestCallOptions) =>
request('PATCH', url, body, callOptions),
delete: (url: string, callOptions?: RequestCallOptions) =>
request('DELETE', url, undefined, callOptions),
};
}