import http from "http"; import https from "https"; import { URL } from "url"; import querystring from "querystring"; import zlib from "zlib"; const parseCookies = (cookieHeader) => { const cookies = {}; if (cookieHeader) { cookieHeader.split(";").forEach(cookie => { const [key, value] = cookie.split("="); if (key && value) cookies[key.trim()] = value.trim(); }); } return cookies; }; const formatCookies = (cookies) => Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; "); const addCookiesToRequest = (cookies, headers) => { headers["Cookie"] = formatCookies(cookies); }; const decompress = (data, encoding) => { return encoding === "br" ? zlib.brotliDecompressSync(data) : encoding === "gzip" ? zlib.gunzipSync(data) : encoding === "deflate" ? zlib.inflateSync(data) : data; }; const readData = (res, mode) => new Promise((resolve, reject) => { const chunks = []; res .on("data", chunk => chunks.push(chunk)) .on("end", () => { try { const data = decompress(Buffer.concat(chunks), res.headers["content-encoding"]); resolve((mode === "json" ? JSON.parse(data.toString("utf8")) : mode === "buffer" ? data : mode === "arraybuffer" ? new Uint8Array(data).buffer : data.toString("utf8"))); } catch (err) { reject(err); } }) .on("error", reject); }); const fetch = async (urlString, options = {}, redirectCount = 0, cookies = {}) => { const { protocol, hostname, pathname, search, port } = new URL(urlString); options.followRedirects = options.followRedirects ?? true; if (options.signal?.aborted) throw new Error("Request aborted"); const body = options.method === "POST" && options.body ? options.headers?.["Content-Type"] === "application/json" ? JSON.stringify(options.body) : querystring.stringify(options.body) : null; const requestOptions = { hostname, port: port || (protocol === "https:" ? 443 : 80), path: pathname + search, method: options.method || "GET", headers: { ...options.headers, ...(body ? { "Content-Length": Buffer.byteLength(body) } : {}), "Accept-Encoding": "br, gzip, deflate" } }; addCookiesToRequest(cookies, requestOptions.headers); return new Promise((resolve, reject) => { const requester = protocol === "https:" ? https : http; const req = requester.request(requestOptions, res => { if (res.headers["set-cookie"]) cookies = parseCookies(res.headers["set-cookie"].join("; ")); if (options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { if (redirectCount >= (options.maxRedirects || 5)) return reject(new Error("Max redirects exceeded")); if (!res.headers.location) return reject(new Error("Redirect location not provided")); const nextUrl = new URL(res.headers.location, urlString); return resolve(fetch(nextUrl.toString(), options, redirectCount + 1, cookies)); } if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) return reject(new Error(`Request failed with status code ${res.statusCode}`)); resolve({ body: res, headers: res.headers, cookies: cookies, text: () => readData(res, "text"), json: () => readData(res, "json"), buffer: () => readData(res, "buffer"), arrayBuffer: () => readData(res, "arraybuffer") }); }); req.setTimeout(options.timeout || 5e3, () => { req.destroy(); reject(new Error("Request timed out")); }); req.on("error", reject); if (options.signal) { options.signal.addEventListener("abort", () => { req.destroy(new Error("Request aborted")); reject(new Error("Request aborted")); }); } if (body) req.write(body); req.end(); }); }; export default fetch;