Skip to content

Commit

Permalink
feat(error handling): add GenericHTTPError to cover the rest of Hypix…
Browse files Browse the repository at this point in the history
…el's error types & increase code coverage to 100%
  • Loading branch information
zikeji committed Nov 7, 2020
1 parent 7c3b0f7 commit 02c92e1
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 9 deletions.
41 changes: 36 additions & 5 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EventEmitter } from "events";
import { IncomingHttpHeaders } from "http";
import { Agent, request, RequestOptions } from "https";
import { URL } from "url";
import { GenericHTTPError } from "./errors/GenericHTTPError";
import { InvalidKeyError } from "./errors/InvalidKeyError";
import { RateLimitError } from "./errors/RateLimitError";
import { FindGuild } from "./methods/findGuild";
Expand Down Expand Up @@ -339,10 +340,17 @@ export class Client extends EventEmitter {
try {
response = await call.execute();
} catch (error) {
if (error instanceof InvalidKeyError || call.retries === this.retries) {
/* istanbul ignore else */
if (
error instanceof InvalidKeyError ||
error instanceof GenericHTTPError ||
/* istanbul ignore next */ call.retries === this.retries
) {
throw error;
}
/* istanbul ignore next */
call.retries += 1;
/* istanbul ignore next */
return this.executeActionableCall<T>(call);
} finally {
this.queue.free();
Expand All @@ -359,6 +367,7 @@ export class Client extends EventEmitter {
/** @internal */
private createActionableCall<T extends Components.Schemas.ApiSuccess>(
path: string,
/* istanbul ignore next */
parameters: Parameters = {}
): ActionableCall<T> {
return {
Expand All @@ -370,7 +379,10 @@ export class Client extends EventEmitter {
/** @internal */
private callMethod<
T extends Components.Schemas.ApiSuccess & { cause?: string }
>(path: string, parameters: Parameters = {}): Promise<T> {
>(
path: string,
/* istanbul ignore next */ parameters: Parameters = {}
): Promise<T> {
const url = new URL(path, Client.endpoint);
Object.keys(parameters).forEach((param) => {
url.searchParams.set(param, parameters[param]);
Expand Down Expand Up @@ -405,6 +417,7 @@ export class Client extends EventEmitter {
incomingMessage.on("end", () => {
this.getRateLimitHeaders(incomingMessage.headers);

/* istanbul ignore next */
if (
typeof responseBody !== "string" ||
responseBody.trim().length === 0
Expand All @@ -420,24 +433,40 @@ export class Client extends EventEmitter {
}

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 (
typeof responseObject === "object" &&
responseObject.cause === "Invalid API key"
/* istanbul ignore next */ responseObject?.cause &&
typeof incomingMessage.statusCode === "number"
) {
throw new InvalidKeyError("Invalid API Key");
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(
Expand All @@ -451,11 +480,13 @@ export class Client extends EventEmitter {
});

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();
Expand Down
9 changes: 9 additions & 0 deletions src/errors/GenericHTTPError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class GenericHTTPError extends Error {
/** The status code of the response */
public code: number;
constructor(code: number, message: string) {
super(message);
this.code = code;
Object.setPrototypeOf(this, GenericHTTPError.prototype);
}
}
5 changes: 5 additions & 0 deletions src/errors/RateLimitError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
/* istanbul ignore file */

export class RateLimitError extends Error {
/**
* Ignore this for code coverage as reproducing a real rate limit error is difficult.
*/
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, RateLimitError.prototype);
Expand Down
47 changes: 44 additions & 3 deletions tests/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
import { expect } from "chai";
import { Client } from "../src";
import { Agent } from "https";
import { Client, InvalidKeyError } from "../src";
import { GenericHTTPError } from "../src/errors/GenericHTTPError";
import { getResultArray } from "../src/util/ResultArray";
import { getResultObject } from "../src/util/ResultObject";

describe("Client throws invali api key", function () {
it("should throw invalid API key", function () {
const client = new Client(process.env.HYPIXEL_KEY || "");
const invalidApiClient = new Client("1234");
const timeoutClient = new Client(process.env.HYPIXEL_KEY || "", {
timeout: 1,
retries: 3,
userAgent: "@test/client",
agent: new Agent({ timeout: 1 }),
});

describe("Client throws invalid api key", function () {
it("should throw invalid API key on construction", function () {
try {
new Client((123 as unknown) as string);
} catch (e) {
expect(e.message).to.equal("Invalid API key");
}
});
it("should throw invalid API key on call", async function () {
try {
await invalidApiClient.guild.id("asda");
} catch (e) {
expect(e).to.be.instanceOf(InvalidKeyError);
}
});
});

describe("Getting a guild by an invalid guild ID", function () {
this.timeout(30000);
this.slow(1000);
it("should throw a GenericHTTPError", async function () {
try {
await client.guild.id("asda");
} catch (e) {
expect(e).instanceOf(GenericHTTPError);
}
});
});

describe("A timeout of 1 should throw configured timeout.", function () {
it("should throw a Error", async function () {
try {
await timeoutClient.guild.id("asda");
} catch (e) {
expect(e).instanceOf(Error);
expect(e.message).to.equal("Hit configured timeout.");
}
});
});

describe("getResultObject throws", function () {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"incremental": true,
"pretty": true,
"strict": true,
"removeComments": true,
"removeComments": false,
"stripInternal": true,
"esModuleInterop": true
},
Expand Down

0 comments on commit 02c92e1

Please sign in to comment.