Files
web-check/api/_common/http.js
2026-05-10 07:16:57 +01:00

116 lines
3.3 KiB
JavaScript

// Thin fetch wrapper matching the axios shape used across the api: opts.params,
// opts.headers, opts.auth, opts.timeout, opts.validateStatus; returns
// { data, status, statusText, headers }; throws errors with response/code
const DEFAULT_TIMEOUT = 60000;
const buildAuth = (auth) => {
if (!auth?.username) return null;
return 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
};
const appendParams = (url, params) => {
if (!params) return url;
const u = new URL(url);
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
return u.href;
};
const headersToObject = (headers) => {
const out = {};
for (const [k, v] of headers.entries()) {
const key = k.toLowerCase();
if (key === 'set-cookie') {
out[key] = headers.getSetCookie ? headers.getSetCookie() : v.split(/, (?=[^;]+=)/);
} else {
out[key] = v;
}
}
return out;
};
// Auto-parse JSON when the response advertises it, fall back to raw text
const parseBody = async (response) => {
const ct = (response.headers.get('content-type') || '').toLowerCase();
const text = await response.text();
if (!text) return ct.includes('json') ? null : '';
if (ct.includes('json')) {
try {
return JSON.parse(text);
} catch {
return text;
}
}
return text;
};
const isOk = (status, validate) => (validate ? validate(status) : status >= 200 && status < 300);
const wrapNetworkError = (error) => {
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
const e = new Error(error.message || 'Request timed out');
e.code = 'ECONNABORTED';
return e;
}
const code = error.cause?.code;
if (code) {
const e = new Error(error.message);
e.code = code;
return e;
}
return error;
};
export const UA =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/120.0.0.0 Safari/537.36 (compatible; web-check/1.0; +https://web-check.xyz)';
const send = async (method, url, body, opts = {}) => {
const finalUrl = appendParams(url, opts.params);
const headers = { 'user-agent': UA, ...opts.headers };
const authHeader = buildAuth(opts.auth);
if (authHeader) headers.authorization = authHeader;
const init = {
method,
headers,
signal: AbortSignal.timeout(opts.timeout || DEFAULT_TIMEOUT),
};
if (body !== undefined && body !== null) {
if (typeof body === 'object') {
init.body = JSON.stringify(body);
const hasCt = Object.keys(headers).some((k) => k.toLowerCase() === 'content-type');
if (!hasCt) init.headers['content-type'] = 'application/json';
} else {
init.body = body;
}
}
let response;
try {
response = await fetch(finalUrl, init);
} catch (error) {
throw wrapNetworkError(error);
}
const data = await parseBody(response);
const result = {
data,
status: response.status,
statusText: response.statusText,
headers: headersToObject(response.headers),
};
if (!isOk(response.status, opts.validateStatus)) {
const err = new Error(`Request failed with status code ${response.status}`);
err.response = result;
throw err;
}
return result;
};
export const httpGet = (url, opts) => send('GET', url, null, opts);
export const httpPost = (url, body, opts) => send('POST', url, body, opts);