define([
"md5",
"models/dataONEServices/UrlBuilder",
"models/dataONEServices/HttpRetryPolicy",
"models/dataONEServices/DataONEHttpError",
"common/Utilities",
], (md5, UrlBuilder, HttpRetryPolicy, DataONEHttpError, Utilities) => {
const { buildUrl } = UrlBuilder;
const { getCaseInsensitive } = Utilities;
/**
* Header names that affect request deduplication. Defaults.
* @type {string[]}
*/
const DEFAULT_HEADER_NAMES_DEDUP = [
"Content-Type",
"Accept",
"Authorization",
// Note we don't use range or conditional get requests yet, but including
// them here for future-proofing.
"Range",
"If-Match",
"If-Modified-Since",
"If-None-Match",
];
/**
* Default allowed HTTP methods.
* @type {string[]}
*/
const DEFAULT_ALLOWED_HTTP_METHODS = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"OPTIONS",
];
/**
* Default supported response types.
* @type {string[]}
*/
const DEFAULT_RESPONSE_TYPES = ["json", "arrayBuffer", "blob", "text"];
/**
* Default options for DataONEHttpClient instances.
* @type {DataONEHttpClientOptions}
*/
const DEFAULT_OPTIONS = {
baseUrl: "",
defaultHeaders: {},
timeoutMs: null,
retry: {},
dedupe: true,
allowedHttpMethods: DEFAULT_ALLOWED_HTTP_METHODS,
headerNamesForDedup: DEFAULT_HEADER_NAMES_DEDUP,
responseTypes: DEFAULT_RESPONSE_TYPES,
};
/**
* Create an AbortError with the provided reason.
* @param {string} [reason] Abort reason, defaults to "Aborted"
* @returns {Error} Error with name set to AbortError
*/
const abortError = (reason = "Aborted") => {
const err = new Error(reason);
err.name = "AbortError";
return err;
};
/**
* Create a TimeoutError with the provided reason.
* @param {string} [reason] Timeout reason, defaults to "Request timed out"
* @returns {Error} Error with name set to TimeoutError
*/
const timeoutError = (reason = "Request timed out") => {
const err = new Error(reason);
err.name = "TimeoutError";
err.code = "ETIMEDOUT";
err.isTimeout = true;
return err;
};
/**
* Delay for a specified duration with optional abort support.
* @param {number} ms Delay duration in milliseconds
* @param {AbortSignal} [signal] Abort signal to cancel the delay
* @returns {Promise<void>} Resolves after the delay or rejects on abort
*/
const sleep = (ms, signal) =>
new Promise((resolve, reject) => {
if (!Number.isFinite(ms) || ms <= 0) {
if (signal?.aborted) {
reject(abortError(signal.reason));
} else {
resolve();
}
return;
}
let timeoutId;
let onAbort; // To avoid defined before use eslint error
const cleanup = () => {
clearTimeout(timeoutId);
signal?.removeEventListener("abort", onAbort);
};
onAbort = () => {
cleanup();
reject(abortError(signal.reason));
};
if (signal?.aborted) {
reject(abortError(signal.reason));
return;
}
signal?.addEventListener("abort", onAbort, { once: true });
timeoutId = setTimeout(() => {
cleanup();
resolve();
}, ms);
});
/**
* Configuration options for {@link DataONEHttpClient}.
* @typedef {object} DataONEHttpClientOptions
* @property {string} [baseUrl] Base URL for relative requests, e.g.
* "https://arcticdata.io/metacat/d1/mn/v2/"
* @property {object} [defaultHeaders] Headers applied to every request
* @property {number|null} [timeoutMs] Default timeout per request (ms). Set
* to null for no timeout.
* @property {object} [retry] Retry configuration, see
* {@link HttpRetryPolicy}
* @property {boolean} [dedupe] Enable in-flight deduplication. If
* true, when an identical request has already been sent and not yet
* returned, the promise for that request will be returned instead of
* creating a new one.
* @property {string[]} [allowedHttpMethods] List of allowed HTTP
* methods
* @property {string[]} [headerNamesForDedup] List of headers to
* include when generating deduplication keys.
* @property {string[]} [responseTypes] List of supported response
* types.
*/
/**
* Options accepted by {@link DataONEHttpClient#request}.
* @typedef {object} DataONEHttpRequestOptions
* @property {string} path Path relative to `baseUrl` (e.g. "/v2/meta/{pid}")
* @property {"text"|"json"|"arrayBuffer"|"blob"} [responseType="text"]
* Expected response type, used for parsing.
* @property {boolean} [encodePath=true] Whether to URL-encode path segments.
* @property {string} [method="GET"] HTTP method.
* @property {object} [headers] Per-request headers, to be merged with
* `defaultHeaders`. `headers` must be a plain object (not a `Headers`
* instance).
* @property {*} [body] Request body.
* @property {string|null} [token] Optional auth token (Bearer).
* @property {AbortSignal} [signal] AbortSignal to cancel the request.
* @property {number|null} [timeoutMs] Override timeout in milliseconds.
* @property {object} [retry] Override retry configuration for this request.
*/
/**
* Normalized request options used internally after defaults and normalization
* have been applied.
* @typedef {object} NormalizedDataONEHttpRequestOptions
* @property {string} url Full request URL.
* @property {string} method Normalized HTTP method.
* @property {"text"|"json"|"arrayBuffer"|"blob"} responseType Normalized
* response type.
* @property {object} headers Effective headers passed to `fetch()` (merged +
* auth).
* @property {*} [body] Request body.
* @property {string|null} [token] Optional auth token (Bearer).
* @property {AbortSignal} [signal] AbortSignal to cancel the request.
* @property {number|null} timeoutMs Effective timeout in milliseconds.
* @property {object} retry Retry overrides for this request.
* @property {HttpRetryPolicy} retryPolicy Effective retry policy for this
* request.
*/
/**
* Normalized HTTP response returned by the client.
* @typedef {object} DataONEHttpResponse
* @property {boolean} ok Whether the HTTP status is in the 200-299 range.
* @property {number} status HTTP status code.
* @property {Headers} headers Response headers.
* @property {string} url Final response URL.
* @property {*} data Parsed response data.
*/
/**
* @class DataONEHttpClient
* @classcategory Models/DataONEServices
* @classdesc Generic HTTP client for DataONE APIs. Handles auth headers,
* retries with backoff, optional in-flight deduplication, abort support,
* normalized responses/errors, and timeouts. It does not know about app
* models or TTLs; it only handles transport concerns.
* @since 2.37.0
*/
class DataONEHttpClient {
/**
* @param {DataONEHttpClientOptions} [options] Client configuration
*/
constructor(options = {}) {
const {
baseUrl,
defaultHeaders,
timeoutMs,
retry,
dedupe,
allowedHttpMethods,
headerNamesForDedup,
responseTypes,
} = this.constructor.normalizeOptions(options);
this.baseUrl = baseUrl;
this.defaultHeaders = defaultHeaders;
this.timeoutMs = timeoutMs;
this.retryPolicy = new HttpRetryPolicy(retry);
this.dedupe = dedupe;
// Map of dedupeKey -> Promise for in-flight requests
this.inFlight = new Map();
// For request deduplication, objects that can't be serialized will be
// assigned unique IDs
this.bodyIds = new WeakMap();
this.bodyIdSeq = 0;
this.allowedHttpMethods = allowedHttpMethods;
this.headerNamesForDedup = headerNamesForDedup;
this.responseTypes = responseTypes;
}
/**
* Normalize options for instance keys and default values.
* @param {DataONEHttpClientOptions} [options] Client configuration
* @returns {object} Normalized options
*/
static normalizeOptions(options = {}) {
const source =
options && typeof options === "object" ? options : DEFAULT_OPTIONS;
const copyObject = (value, fallback = {}) =>
value && typeof value === "object" ? { ...value } : { ...fallback };
const copyArray = (value, fallback = []) =>
Array.isArray(value) ? [...value] : [...fallback];
const toUpper = (item) => String(item).toUpperCase();
const toLower = (item) => String(item).toLowerCase();
const dedupe =
source.dedupe === undefined ? DEFAULT_OPTIONS.dedupe : source.dedupe;
if (typeof dedupe !== "boolean") {
throw new Error("dedupe must be a boolean");
}
const rawTimeoutMs =
source.timeoutMs === undefined
? DEFAULT_OPTIONS.timeoutMs
: source.timeoutMs;
const timeoutMs = this.normalizeTimeoutMs(rawTimeoutMs);
return {
...DEFAULT_OPTIONS,
...source,
baseUrl: source.baseUrl
? Utilities.normalizeUrl(source.baseUrl)
: DEFAULT_OPTIONS.baseUrl,
timeoutMs,
dedupe,
// For arrays and objects, we want to ensure we create new instances so
// that they can be safely mutated without affecting the defaults or
// other instances that use the same defaults.
defaultHeaders: copyObject(
source.defaultHeaders,
DEFAULT_OPTIONS.defaultHeaders,
),
retry: copyObject(source.retry, DEFAULT_OPTIONS.retry),
allowedHttpMethods: copyArray(
source.allowedHttpMethods,
DEFAULT_OPTIONS.allowedHttpMethods,
).map(toUpper),
headerNamesForDedup: copyArray(
source.headerNamesForDedup,
DEFAULT_OPTIONS.headerNamesForDedup,
).map(toLower),
responseTypes: copyArray(
source.responseTypes,
DEFAULT_OPTIONS.responseTypes,
).map(toLower),
};
}
/**
* Normalize a timeout value to either a valid number or null.
* @param {number|null|undefined} timeoutMs Timeout in milliseconds
* @returns {number|null} Normalized timeout
* @throws {Error} If timeout is not null/undefined and not a non-negative number
* @private
*/
static normalizeTimeoutMs(timeoutMs) {
if (timeoutMs === null || timeoutMs === undefined) return null;
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
throw new Error("timeoutMs must be a non-negative number or null");
}
return timeoutMs;
}
/**
* Build a unique instance key based on client options.
* @param {DataONEHttpClientOptions} [options] Client configuration
* @returns {string} Instance key
*/
static buildInstanceKey(options = {}) {
const normalizedOptions = this.normalizeOptions(options);
const keyFields = [
"baseUrl",
"timeoutMs",
"dedupe",
"defaultHeaders",
"retry",
"allowedHttpMethods",
"headerNamesForDedup",
"responseTypes",
];
const normalizers = {
baseUrl: Utilities.normalizeUrl,
defaultHeaders: Utilities.stableStringify,
retry: Utilities.stableStringify,
allowedHttpMethods: Utilities.stableStringify,
headerNamesForDedup: Utilities.stableStringify,
responseTypes: Utilities.stableStringify,
};
return Utilities.buildInstanceKey(
normalizedOptions,
keyFields,
normalizers,
);
}
/**
* Get a singleton DataONEHttpClient instance for the provided options.
* @param {DataONEHttpClientOptions} [options] Client configuration
* @returns {DataONEHttpClient} Client instance
*/
static get(options = {}) {
return Utilities.getSingleton(
this,
options,
this.buildInstanceKey.bind(this),
);
}
/**
* Normalize and merge request options with client defaults. Also check for
* required options.
* @param {DataONEHttpRequestOptions} options Raw request options
* @returns {NormalizedDataONEHttpRequestOptions} Normalized request options
* @private
*/
normalizeRequestOptions(options = {}) {
const {
path,
method = "GET",
responseType = "text",
encodePath = true,
headers,
body,
token = null,
signal,
timeoutMs: requestTimeoutMs,
retry: retryOverrides = {},
} = options;
if (typeof path !== "string") {
throw new Error("A request path is required");
}
const normalizedMethod = this.normalizeMethod(method);
const normalizedResponseType = this.normalizeResponseType(responseType);
const url = buildUrl(this.baseUrl, path, encodePath);
const mergedHeaders = this.constructor.mergeHeaders(
this.defaultHeaders,
headers,
);
if (token && !getCaseInsensitive(mergedHeaders, "Authorization")) {
mergedHeaders.Authorization = `Bearer ${token}`;
}
const hasRequestTimeoutOverride =
requestTimeoutMs !== undefined && requestTimeoutMs !== null;
const timeoutSource = hasRequestTimeoutOverride
? requestTimeoutMs
: this.timeoutMs;
const normalizedTimeoutMs =
this.constructor.normalizeTimeoutMs(timeoutSource);
const retry =
retryOverrides && typeof retryOverrides === "object"
? retryOverrides
: {};
const retryPolicy = this.retryPolicy.withOverrides(retry);
return {
url,
method: normalizedMethod,
responseType: normalizedResponseType,
headers: mergedHeaders,
body,
token,
signal,
timeoutMs: normalizedTimeoutMs,
retry,
retryPolicy,
};
}
/**
* Normalize a responseType option to a supported value.
* @param {string} responseType Raw response type
* @returns {"json"|"arrayBuffer"|"blob"|"text"} Normalized response type
* @private
*/
normalizeResponseType(responseType) {
if (typeof responseType !== "string") {
throw new Error("responseType must be a string");
}
const normalizedResponseType = responseType.toLowerCase();
if (this.responseTypes.includes(normalizedResponseType))
return normalizedResponseType;
// Otherwise throw an error
throw new Error(
`Invalid responseType: ${responseType}. ` +
`Allowed types: ${this.responseTypes.join(", ")}`,
);
}
/**
* Merge two header objects case-insensitively. Headers in requestHeaders
* override defaults regardless of key casing.
* @param {object} defaultHeaders Default headers
* @param {object} requestHeaders Request-specific headers
* @returns {object} Merged headers object
* @private
*/
static mergeHeaders(defaultHeaders = {}, requestHeaders = {}) {
const merged = {};
const keyByLower = Object.create(null);
const addHeaders = (headers) => {
if (!headers || typeof headers !== "object") return;
Object.entries(headers).forEach(([key, value]) => {
const normalizedKey = String(key).toLowerCase();
const existingKey = keyByLower[normalizedKey];
if (existingKey && existingKey !== key) {
delete merged[existingKey];
}
keyByLower[normalizedKey] = key;
merged[key] = value;
});
};
addHeaders(defaultHeaders);
addHeaders(requestHeaders);
return merged;
}
/**
* Normalize an HTTP method to an allowed uppercase value.
* @param {string} method Raw HTTP method
* @returns {string} Normalized HTTP method
* @throws {Error} If method is not provided or invalid
* @private
*/
normalizeMethod(method) {
if (typeof method !== "string") {
throw new Error("method must be provided");
}
const normalizedMethod = method.toUpperCase();
if (this.allowedHttpMethods.includes(normalizedMethod)) {
return normalizedMethod;
}
throw new Error(
`Invalid HTTP method: ${method}. ` +
`Allowed methods: ${this.allowedHttpMethods.join(", ")}`,
);
}
/**
* Perform an HTTP request.
* @param {DataONEHttpRequestOptions} options Request options
* @returns {Promise<DataONEHttpResponse>} A promise that resolves to a
* normalized response object.
*/
async request(options = {}) {
const requestOptions = this.normalizeRequestOptions(options);
let dedupKey = null;
if (this.dedupe) {
dedupKey = this.generateRequestKey(requestOptions);
const inFlight = this.checkForInFlightRequest(dedupKey);
if (inFlight) return inFlight;
}
const job = this.executeRequest(requestOptions);
if (this.dedupe) {
this.addToInFlight(dedupKey, job);
}
try {
return await job;
} finally {
this.removeFromInFlight(dedupKey);
}
}
/**
* Return an in-flight request promise for a dedupe key, if present.
* @param {string|null} dedupKey Dedupe key for the request
* @returns {Promise<DataONEHttpResponse>|null} In-flight promise or null
* @private
*/
checkForInFlightRequest(dedupKey) {
if (this.dedupe === false || this.inFlight.size === 0) return null;
return this.inFlight.get(dedupKey) || null;
}
/**
* Store an in-flight request promise for deduplication.
* @param {string|null} dedupKey Dedupe key for the request
* @param {Promise<DataONEHttpResponse>} promise Request promise
* @private
*/
addToInFlight(dedupKey, promise) {
if (!dedupKey || !promise || this.dedupe === false) return;
this.inFlight.set(dedupKey, promise);
}
/**
* Remove a request from the in-flight map.
* @param {string|null} dedupKey Dedupe key for the request
* @private
*/
removeFromInFlight(dedupKey) {
if (!dedupKey) return;
if (this.inFlight.size === 0) return;
this.inFlight.delete(dedupKey);
}
/**
* Fetch with retry and timeouts. Do not call directly; use
* {@link DataONEHttpClient#request} instead.
* @param {NormalizedDataONEHttpRequestOptions} options Request options
* @returns {Promise<DataONEHttpResponse>} A promise that resolves to a
* normalized response object.
* @private
*/
async executeRequest(options = {}) {
const { url, retryPolicy, signal } = options;
const { maxAttempts } = retryPolicy;
let attempt = 0;
let lastError;
/* eslint-disable no-await-in-loop */
while (attempt < maxAttempts) {
attempt += 1;
try {
const response = await this.performFetch(options);
if (response.ok) return response;
// Convert non-OK HTTP into a normalized error path
throw new DataONEHttpError({
response,
attempt,
url,
message: `Request failed with status ${response.status}`,
status: response.status,
});
} catch (err) {
// Stop all processing and throw immediately if the request was
// aborted by the caller
if (signal?.aborted && err?.name !== "TimeoutError") {
throw abortError(signal.reason);
}
const normalized =
err instanceof DataONEHttpError
? err
: new DataONEHttpError({
error: err,
url,
attempt,
networkError: err?.name === "TimeoutError" || undefined,
});
lastError = normalized;
const shouldRetry = retryPolicy.shouldRetry({
attempt,
maxAttempts,
status: normalized.status,
isNetworkError: normalized.networkError === true,
});
if (!shouldRetry) throw normalized;
const retryAfterMs = retryPolicy.parseRetryAfter(normalized.headers);
const delay = retryPolicy.computeDelay({ attempt, retryAfterMs });
await sleep(delay, signal);
}
}
/* eslint-enable no-await-in-loop */
throw lastError;
}
/**
* Performs the actual fetch call. Handles timeouts and aborts. Do not call
* directly; use {@link DataONEHttpClient#request} instead.
* @param {NormalizedDataONEHttpRequestOptions} options Request options
* @returns {Promise<DataONEHttpResponse>} A promise that resolves to a
* normalized response object.
* @private
*/
async performFetch(options = {}) {
const { url, method, headers, body, responseType, signal, timeoutMs } =
options;
// Enable fetch to be aborted via AbortController. We track whether an
// abort came from the caller's signal vs an internal timeout, so we can
// reliably convert timeouts into TimeoutError.
const controller = new AbortController();
let abortedByCaller = false;
const onAbort = () => {
abortedByCaller = true;
controller.abort();
};
if (signal) {
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener("abort", onAbort, { once: true });
}
}
// Abort the fetch if it exceeds the timeout
let timeoutId = null;
let timedOut = false;
let timeoutErr = null;
const effectiveTimeout =
timeoutMs !== undefined ? timeoutMs : this.timeoutMs;
if (effectiveTimeout && effectiveTimeout > 0) {
timeoutErr = timeoutError();
timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, effectiveTimeout);
}
try {
const res = await fetch(url, {
method,
headers,
body,
signal: controller.signal,
});
const data = await this.constructor.readBody(res, responseType);
return {
ok: res.ok,
status: res.status,
headers: res.headers,
url: res.url,
data,
};
} catch (error) {
// Guarantee that internal timeouts surface as TimeoutError.
if (timedOut) {
timeoutErr.cause = error;
throw timeoutErr;
}
// If the caller aborted, preserve abort semantics.
if (abortedByCaller) {
throw abortError(signal?.reason);
}
throw error;
} finally {
if (timeoutId) clearTimeout(timeoutId);
signal?.removeEventListener("abort", onAbort);
}
}
/**
* Generate a key to differentiate requests for deduplication. The strategy
* veers towards less deduplication in favour of ensureing we don't
* incorrectly dedupe requests that are actually different. For example, a
* request that sets the token via headers will be considered different from
* one that sets it via the token option, for simplicity.
* @param {NormalizedDataONEHttpRequestOptions} options Request options
* @returns {string} A hash key representing the request
* @private
*/
generateRequestKey({
token,
url,
method,
body,
headers,
responseType,
timeoutMs,
retry,
signal,
}) {
// Generate scope based on token or Authorization header
let scope = token ? `auth:${md5(String(token))}` : null;
if (!scope) {
const authHeader = getCaseInsensitive(headers, "Authorization");
if (authHeader) {
scope = `auth:${md5(authHeader)}`;
} else {
scope = "public";
}
}
const bodyNormalized = this.normalizeBodyForKey(body);
const headerRepresentation = this.normalizeHeadersForKey(headers);
const timeoutKey =
timeoutMs === null || timeoutMs === undefined
? "none"
: String(timeoutMs);
const retryKey = Utilities.stableStringify(retry);
const signalKey = signal ? this.getBodyId(signal) : "none";
const keyParts = [
scope,
String(method).toUpperCase(),
url,
responseType,
headerRepresentation,
`timeout:${timeoutKey}`,
`retry:${retryKey}`,
`signal:${signalKey}`,
bodyNormalized,
];
return md5(keyParts.join("|"));
}
/**
* Read a response body according to the requested response type.
* @param {Response} response Fetch response
* @param {"text"|"json"|"arrayBuffer"|"blob"} responseType Expected body
* type
* @returns {Promise<*>} Parsed response data
* @private
*/
static async readBody(response, responseType) {
const responseCopy = response.clone();
try {
switch (responseType.toLocaleLowerCase()) {
case "json":
return await response.json();
case "arraybuffer":
return await response.arrayBuffer();
case "blob":
return await response.blob();
case "text":
default:
return await response.text();
}
} catch (e) {
// If parsing fails, fall back to text where possible
if (responseType !== "text") {
return responseCopy.text();
}
throw e;
}
}
/**
* Convert a body sent with a request into a string suitable for generating
* a deduplication key. It will be assigned an ID if it's a non-serializable
* type. If the same object is used in multiple requests, it will have the
* same ID.
* @param {*} body Request body
* @returns {string} Normalized body representation
* @private
*/
normalizeBodyForKey(body) {
if (body === null || body === undefined) return "";
if (typeof body === "string") return `str:${md5(body)}`;
if (body instanceof URLSearchParams) {
// Stable string representation
return `usp:${body.toString()}`;
}
// Non-serializable / potentially large bodies: use identity
if (body instanceof FormData) {
return `fd:${this.getBodyId(body)}`;
}
let prefix = "";
if (body instanceof Blob || body instanceof File) {
const type = body.type || "unknown";
const size = typeof body.size === "number" ? body.size : 0;
prefix = `blob:${type}:${size}`;
}
// Binary views / buffers: avoid hashing bytes
if (ArrayBuffer?.isView?.(body) || body instanceof ArrayBuffer) {
const byteLength =
typeof body.byteLength === "number" ? body.byteLength : 0;
prefix = `bin:${byteLength}`;
}
if (prefix) {
return `${prefix}:${this.getBodyId(body)}`;
}
// Try to JSON serialize other body types with stable key ordering.
try {
const json = Utilities.stableStringify(body, {
ignoreCase: false,
orderMatters: true,
});
return `json:${md5(json)}`;
} catch (e) {
return `obj:${this.getBodyId(body)}`;
}
}
/**
* Return a stable ID for non-serializable bodies.
* @param {*} value Request body value
* @returns {string} Stable object ID
* @private
*/
getBodyId(value) {
if (!this.bodyIds.has(value)) {
this.bodyIds.set(value, `obj:${this.bodyIdSeq + 1}`);
this.bodyIdSeq += 1;
}
return this.bodyIds.get(value);
}
/**
* Generate a string representation of headers suitable for generating a
* deduplication key. Avoid too much deduplication by only including certain
* headers.
* @param {object} headers Request headers
* @returns {string} Normalized headers representation
* @private
*/
normalizeHeadersForKey(headers) {
if (!headers || typeof headers !== "object") return "";
const parts = this.headerNamesForDedup.map((name) => {
let value = getCaseInsensitive(headers, name) || "";
if (Array.isArray(value)) value = value.join(",");
else if (typeof value === "object") {
try {
value = Utilities.stableStringify(value);
} catch (e) {
value = this.getBodyId(value);
}
}
value = String(value).trim();
if (!value) return "";
return `${name.toLowerCase()}:${value}`;
});
return parts.join("|");
}
}
DataONEHttpClient.DEFAULT_RETRY = HttpRetryPolicy.DEFAULT_RETRY;
DataONEHttpClient.DEFAULT_RETRY_ON = HttpRetryPolicy.DEFAULT_RETRY_ON;
DataONEHttpClient.DEFAULT_HEADER_NAMES_DEDUP = DEFAULT_HEADER_NAMES_DEDUP;
DataONEHttpClient.ALLOWED_HTTP_METHODS = DEFAULT_ALLOWED_HTTP_METHODS;
DataONEHttpClient.RESPONSE_TYPES = DEFAULT_RESPONSE_TYPES;
DataONEHttpClient.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
DataONEHttpClient.Error = DataONEHttpError;
DataONEHttpClient.instances = new Map();
return DataONEHttpClient;
});