BetterDiscordApp-rauenzi/preload/src/api/fetch.js

149 lines
5.0 KiB
JavaScript
Raw Normal View History

2023-04-12 23:20:20 +02:00
import * as https from "https";
import * as http from "http";
const MAX_DEFAULT_REDIRECTS = 20;
2023-04-12 23:20:20 +02:00
const redirectCodes = new Set([301, 302, 307, 308]);
/**
* @typedef {Object} FetchOptions
* @property {"GET" | "PUT" | "POST" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD" | "CONNECT" | "TRACE"} [method] - Request method.
* @property {Record<string, string>} [headers] - Request headers.
* @property {"manual" | "follow"} [redirect] - Whether to follow redirects.
* @property {number} [maxRedirects] - Maximum amount of redirects to be followed.
* @property {AbortSignal} [signal] - Signal to abruptly cancel the request
* @property {Uint8Array | string} [body] - Defines a request body. Data must be serializable.
* @property {number} [timeout] - Request timeout time.
*/
/**
* @param {string} url
* @param {FetchOptions} options
*/
2023-04-12 23:20:20 +02:00
export function nativeFetch(url, options) {
let state = "PENDING";
const data = {content: [], headers: null, statusCode: null, url: url, statusText: "", redirected: false};
const listeners = new Set();
const errors = new Set();
/** * @param {URL} url */
const execute = (url, options, redirectCount = 0) => {
2023-08-06 04:16:45 +02:00
const Module = url.protocol === "http:" ? http : https;
2023-04-12 23:20:20 +02:00
const req = Module.request(url.href, {
headers: options.headers ?? {},
2023-08-06 04:16:45 +02:00
method: options.method ?? "GET",
timeout: options.timeout ?? 3000
2023-04-12 23:20:20 +02:00
}, res => {
if (redirectCodes.has(res.statusCode) && res.headers.location && options.redirect !== "manual") {
redirectCount++;
if (redirectCount >= (options.maxRedirects ?? MAX_DEFAULT_REDIRECTS)) {
state = "ABORTED";
const error = new Error(`Maximum amount of redirects reached (${options.maxRedirects ?? MAX_DEFAULT_REDIRECTS})`);
errors.forEach(e => e(error));
return;
}
let final;
try {
final = new URL(res.headers.location);
}
catch (error) {
state = "ABORTED";
errors.forEach(e => e(error));
return;
}
2023-04-12 23:20:20 +02:00
for (const [key, value] of new URL(url).searchParams.entries()) {
final.searchParams.set(key, value);
}
return execute(final, options, redirectCount);
2023-04-12 23:20:20 +02:00
}
res.on("data", chunk => data.content.push(chunk));
res.on("end", () => {
data.content = Buffer.concat(data.content);
data.headers = res.headers;
data.statusCode = res.statusCode;
data.url = url.toString();
data.statusText = res.statusMessage;
data.redirected = redirectCount > 0;
2023-04-12 23:20:20 +02:00
state = "DONE";
listeners.forEach(listener => listener());
});
res.on("error", error => {
state = "ABORTED";
errors.forEach(e => e(error));
});
});
2023-08-06 04:16:45 +02:00
req.on("timeout", () => {
const error = new Error("Request timed out");
req.destroy(error);
});
req.on("error", error => {
state = "ABORTED";
errors.forEach(e => e(error));
});
2023-04-12 23:20:20 +02:00
if (options.body) {
2023-08-06 04:16:45 +02:00
try {req.write(options.body);}
2023-04-12 23:20:20 +02:00
catch (error) {
state = "ABORTED";
errors.forEach(e => e(error));
}
finally {
2023-04-12 23:20:20 +02:00
req.end();
}
}
else {
2023-04-12 23:20:20 +02:00
req.end();
}
if (options.signal) {
options.signal.addEventListener("abort", () => {
req.end();
state = "ABORTED";
});
}
};
2023-08-06 04:16:45 +02:00
/**
* Obviously parsing a URL may throw an error, but this is
* actually intended here. The caller should handle this
* gracefully.
*
* Reasoning: at this point the caller does not have a
* reference to the object below so they have no way of
* listening to the error through onError.
*/
const parsed = new URL(url);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(`Unsupported protocol: ${parsed.protocol}`);
}
2023-08-06 04:16:45 +02:00
execute(parsed, options);
2023-04-12 23:20:20 +02:00
return {
onComplete(listener) {
listeners.add(listener);
},
onError(listener) {
errors.add(listener);
},
readData() {
switch (state) {
case "PENDING":
throw new Error("Cannot read data before request is done!");
case "ABORTED":
throw new Error("Request was aborted.");
case "DONE":
return data;
}
}
};
}