amanpay / web /src /api /client.ts
MHamdan's picture
CI deploy 6ffe31f
45a105b verified
Raw
History Blame Contribute Delete
2.7 kB
import type { ApiError } from '../types'
import { getToken } from '../auth/session'
// Base URL from a Vite env var (never a secret). Default '' = same origin as the app.
const BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? ''
const TIMEOUT_MS = Number(import.meta.env.VITE_API_TIMEOUT ?? 15000)
export class ApiRequestError extends Error {
status: number
requestId?: string
constructor(err: ApiError) {
super(err.message)
this.name = 'ApiRequestError'
this.status = err.status
this.requestId = err.requestId
}
}
let lastRequestId: string | null = null
/** The X-Request-ID of the most recent response — shown only in a support/debug panel. */
export function getLastRequestId(): string | null {
return lastRequestId
}
export interface RequestOptions {
method?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: any
headers?: Record<string, string>
signal?: AbortSignal
}
export async function apiRequest<T>(path: string, opts: RequestOptions = {}): Promise<T> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
const headers: Record<string, string> = { Accept: 'application/json', ...opts.headers }
if (opts.body !== undefined) headers['Content-Type'] = 'application/json'
const token = getToken()
if (token) headers['Authorization'] = `Bearer ${token}`
let res: Response
try {
res = await fetch(BASE + path, {
method: opts.method ?? (opts.body !== undefined ? 'POST' : 'GET'),
headers,
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
credentials: 'include', // carry HTTP-only cookies if the backend uses them
signal: opts.signal ?? controller.signal,
})
} catch (e) {
clearTimeout(timer)
const aborted = e instanceof DOMException && e.name === 'AbortError'
throw new ApiRequestError({ status: 0, message: aborted ? 'request timed out' : 'network error' })
}
clearTimeout(timer)
const requestId = res.headers.get('x-request-id') ?? undefined
if (requestId) lastRequestId = requestId
const text = await res.text()
let data: unknown = null
if (text) {
try {
data = JSON.parse(text)
} catch {
if (!res.ok) throw new ApiRequestError({ status: res.status, message: 'malformed response', requestId })
data = null
}
}
if (!res.ok) {
const detail =
data && typeof data === 'object' && 'detail' in data
? String((data as { detail: unknown }).detail)
: `request failed (${res.status})`
throw new ApiRequestError({ status: res.status, message: detail, requestId })
}
return data as T
}