UNPKG

fhir-kit-client

Version:
173 lines 6.27 kB
import HttpAgent from 'agentkeepalive'; import { logRequestError, logRequestInfo, logResponseInfo } from './logging.js'; import { REQUEST_KEY, RESPONSE_KEY } from './types.js'; const { HttpsAgent } = HttpAgent; const defaultHeaders = { accept: 'application/fhir+json' }; /** * Ensures the signal is a native AbortSignal. * * On Node 24+, undici's Request constructor enforces a strict * `instanceof AbortSignal` check. Polyfill libraries such as * `node-abort-controller` create their own AbortSignal class that is NOT * the native one, causing a TypeError at request-build time. We bridge any * foreign signal to a native AbortController so undici is always satisfied. * * See: https://github.com/Vermonster/fhir-kit-client/issues/204 */ function normalizeSignal(signal) { if (signal instanceof AbortSignal) return signal; const controller = new AbortController(); if (signal.aborted) { controller.abort(signal.reason); } else { signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true }); } return controller.signal; } function stringifyBody(body) { if (body === undefined) return undefined; if (typeof body === 'string') return body; return JSON.stringify(body); } function lcKeys(obj) { if (!obj) return {}; return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.toLowerCase(), v])); } function buildResponseError(config) { const error = Object.assign(new Error(`HTTP ${config.status}: ${config.method} ${config.url}`), { response: { status: config.status, data: config.data }, config, }); logRequestError(error); return error; } /** * Internal HTTP client. Use Client methods instead of this class directly. */ export class HttpClient { baseUrlValue; customHeaders; baseRequestOptions; requestSigner; authHeader = {}; /** Keepalive agents keyed by base URL, reused across requests on this instance. */ agentCache = new Map(); constructor({ baseUrl, customHeaders = {}, requestOptions = {}, requestSigner, }) { this.baseUrlValue = ''; this.baseUrl = baseUrl; this.customHeaders = customHeaders; this.baseRequestOptions = requestOptions; this.requestSigner = requestSigner; } set baseUrl(url) { if (!url) throw new Error('baseUrl cannot be blank'); if (typeof url !== 'string') throw new Error('baseUrl must be a string'); this.baseUrlValue = url; } get baseUrl() { return this.baseUrlValue; } set bearerToken(token) { this.authHeader = { authorization: `Bearer ${token}` }; } static responseFor(requestResponse) { return requestResponse[RESPONSE_KEY]; } static requestFor(requestResponse) { return requestResponse[REQUEST_KEY]; } mergeHeaders(requestHeaders) { return { ...lcKeys(defaultHeaders), ...lcKeys(this.authHeader), ...lcKeys(this.customHeaders), ...lcKeys(requestHeaders), }; } buildAgent() { const cached = this.agentCache.get(this.baseUrl); if (cached) return cached; const agent = this.baseUrl.startsWith('https') ? { agent: new HttpsAgent() } : { agent: new HttpAgent() }; this.agentCache.set(this.baseUrl, agent); return agent; } expandUrl(url = '') { if (url.toLowerCase().startsWith('http')) return url; if (this.baseUrl.endsWith('/') && url.startsWith('/')) return this.baseUrl + url.slice(1); if (this.baseUrl.endsWith('/') || url.startsWith('/')) return this.baseUrl + url; return `${this.baseUrl}/${url}`; } buildRequest(method, url, options, body) { const requestInit = { ...this.baseRequestOptions, method, body: stringifyBody(body), headers: new Headers(this.mergeHeaders(options.headers)), keepalive: true, ...this.buildAgent(), }; if (options.signal) requestInit.signal = normalizeSignal(options.signal); if (this.requestSigner) { this.requestSigner(url, requestInit); } return new Request(url, requestInit); } async request(method, requestUrl, options = {}, body) { const url = this.expandUrl(requestUrl); const req = this.buildRequest(method, url, options, body); logRequestInfo(method, url, req.headers); const response = await fetch(req); const { status } = response; logResponseInfo({ status }); const bodyText = await response.text(); let data = {}; if (bodyText) { try { data = JSON.parse(bodyText); } catch { const err = buildResponseError({ status, data: bodyText, method, headers: response.headers, url }); throw err; } } if (!response.ok) { throw buildResponseError({ status, data, method, headers: response.headers, url }); } const result = data; Object.defineProperty(result, RESPONSE_KEY, { writable: false, enumerable: false, value: response }); Object.defineProperty(result, REQUEST_KEY, { writable: false, enumerable: false, value: req }); return result; } async get(url, options) { return this.request('GET', url, options); } async delete(url, options) { return this.request('DELETE', url, options); } async put(url, body, options = {}) { const headers = { 'content-type': 'application/fhir+json', ...lcKeys(options.headers) }; return this.request('PUT', url, { ...options, headers }, body); } async post(url, body, options = {}) { const headers = { 'content-type': 'application/fhir+json', ...lcKeys(options.headers) }; return this.request('POST', url, { ...options, headers }, body); } async patch(url, body, options = {}) { return this.request('PATCH', url, options, body); } } //# sourceMappingURL=http-client.js.map