@zikeji/hypixel
Version:
With IntelliSense support & test coverage, this is an unopinionated async/await API wrapper for Hypixel's Public API. It is developed in TypeScript complete with documentation, typed interfaces for all API responses, built-in rate-limit handling, flexible
147 lines (129 loc) • 4.62 kB
text/typescript
import { request as httpsRequest } from "https";
import { GenericHTTPError } from "../errors/GenericHTTPError";
import { InvalidKeyError } from "../errors/InvalidKeyError";
import { RateLimitError } from "../errors/RateLimitError";
import type { RequestOptions } from "../Client";
import type { DefaultMeta } from "../types/DefaultMeta";
/** @internal */
const CACHE_CONTROL_REGEX = /s-maxage=(\d+)/;
/** @internal */
export function request<
T extends Record<string, unknown> & {
cause?: string;
} & { cloudflareCache?: DefaultMeta["cloudflareCache"] }
>(options: RequestOptions): Promise<T> {
return new Promise((resolve, reject) => {
const clientRequest = httpsRequest(
options.url,
{
method: "GET",
timeout: options.timeout,
headers: {
"User-Agent": options.userAgent,
Accept: "application/json",
},
},
(incomingMessage) => {
let responseBody = "";
incomingMessage.on("data", (chunk) => {
responseBody += chunk;
});
incomingMessage.on("end", () => {
if (!options.noRateLimit) {
options.getRateLimitHeaders(
incomingMessage.headers as Record<string, string>
);
}
/* istanbul ignore next */
if (
typeof responseBody !== "string" ||
responseBody.trim().length === 0
) {
return reject(new Error(`No response body received.`));
}
let responseObject: T | undefined;
try {
responseObject = JSON.parse(responseBody);
} catch (_) {
// noop
}
if (incomingMessage.statusCode !== 200) {
/* istanbul ignore next */
if (incomingMessage.statusCode === 429) {
return reject(new RateLimitError(`Hit key throttle.`));
}
if (incomingMessage.statusCode === 403) {
return reject(new InvalidKeyError("Invalid API Key"));
}
/* istanbul ignore else */
if (
/* istanbul ignore next */ responseObject?.cause &&
typeof incomingMessage.statusCode === "number"
) {
return reject(
new GenericHTTPError(
incomingMessage.statusCode,
responseObject.cause
)
);
}
/**
* Generic catch all that probably should never be caught.
*/
/* istanbul ignore next */
return reject(
new Error(
`${incomingMessage.statusCode} ${incomingMessage.statusMessage}. Response: ${responseBody}`
)
);
}
/* istanbul ignore if */
if (typeof responseObject === "undefined") {
return reject(
new Error(
`Invalid JSON response received. Response: ${responseBody}`
)
);
}
/* istanbul ignore else */
if (incomingMessage.headers["cf-cache-status"]) {
const age = parseInt(incomingMessage.headers.age as string, 10);
const maxAge = CACHE_CONTROL_REGEX.exec(
incomingMessage.headers["cache-control"] as string
);
responseObject.cloudflareCache = {
status: incomingMessage.headers["cf-cache-status"] as never,
...(typeof age === "number" && !Number.isNaN(age) && { age }),
...(incomingMessage.headers["cf-cache-status"] === "HIT" &&
(typeof age !== "number" ||
Number.isNaN(age)) && /* istanbul ignore next */ { age: 0 }),
...(maxAge &&
typeof maxAge === "object" &&
maxAge.length === 2 &&
parseInt(maxAge[1], 10) > 0 && {
maxAge: parseInt(maxAge[1], 10),
}),
};
}
return resolve(responseObject);
});
}
);
let abortError: Error;
/* istanbul ignore next */
clientRequest.once("abort", () => {
abortError = abortError ?? new Error("Client aborted this request.");
reject(abortError);
});
/* istanbul ignore next */
clientRequest.once("error", (error) => {
abortError = error;
clientRequest.abort();
});
clientRequest.setTimeout(options.timeout, () => {
abortError = new Error("Hit configured timeout.");
clientRequest.abort();
});
clientRequest.end();
});
}