Skip to content

Commit

Permalink
🦺 feat(core): add timeout to http client
Browse files Browse the repository at this point in the history
  • Loading branch information
Helloyunho committed May 18, 2024
1 parent f139193 commit 84f43e2
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 4 deletions.
51 changes: 49 additions & 2 deletions core/src/rest/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
export class DiscordAPIError extends Error {}
export class DiscordAPIError extends Error {
status: number;
body: string;

export class HTTPError extends Error {}
constructor(
status = 400,
body: string,
...params: ConstructorParameters<typeof Error>
) {
super(...params);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, DiscordAPIError);
}

this.name = "DiscordAPIError";
this.status = status;
this.body = body;
}
}

export class DiscordAPIInternalError extends Error {
status: number;

constructor(status = 500, ...params: ConstructorParameters<typeof Error>) {
super(...params);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, DiscordAPIInternalError);
}

this.name = "DiscordAPIInternalError";
this.status = status;
}
}

export class DiscordAPITimeoutError extends Error {
timeout: number;

constructor(timeout = 10000, ...params: ConstructorParameters<typeof Error>) {
super(...params);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, DiscordAPITimeoutError);
}

this.name = "DiscordAPITimeoutError";
this.timeout = timeout;
}
}
31 changes: 29 additions & 2 deletions core/src/rest/http_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import {
} from "../../../types/src/constants.ts";
import { Endpoint } from "../../../types/src/endpoints.ts";
import { getRouteBucket } from "./bucket.ts";
import {
DiscordAPIError,
DiscordAPIInternalError,
DiscordAPITimeoutError,
} from "./errors.ts";

export type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

Expand Down Expand Up @@ -54,6 +59,9 @@ export interface HTTPClientOptions {

/** Max request retries. Defaults to 5. */
maxRetries?: number;

/** Request timeout in milliseconds. Defaults to 10000. */
timeout?: number;
}

export class Queue {
Expand Down Expand Up @@ -107,6 +115,7 @@ export class HTTPClient implements HTTPClientOptions {
baseURL: string;
version: number;
maxRetries = 5;
timeout = 10000;

private queue = new Map<string, Queue>();

Expand All @@ -117,6 +126,7 @@ export class HTTPClient implements HTTPClientOptions {
this.baseURL = options?.baseURL ?? DISCORD_API_BASE;
this.version = options?.version ?? DISCORD_API_VERSION;
this.maxRetries = options?.maxRetries ?? this.maxRetries;
this.timeout = options?.timeout ?? this.timeout;
}

async request<T>(
Expand Down Expand Up @@ -166,15 +176,23 @@ export class HTTPClient implements HTTPClientOptions {
}
}

const abortController = new AbortController();
const timeoutID = setTimeout(() => {
abortController.abort();
}, this.timeout);

const res = await fetch(
`${this.baseURL}/v${this.version}${endpoint}`,
{
method,
headers,
body,
signal: abortController.signal,
},
);

clearTimeout(timeoutID);

const resetTime = Date.now() +
Number(res.headers.get("X-RateLimit-Reset-After")) * 1000 + 5; // add a little bit of delay to avoid 429s
queue.resetTime = resetTime;
Expand All @@ -192,12 +210,15 @@ export class HTTPClient implements HTTPClientOptions {
return res.json();
} else if (res.status >= 500 && res.status < 600) {
await res.body?.cancel();
throw new Error(
throw new DiscordAPIInternalError(
res.status,
`Discord API Internal Server Error (${res.status})`,
);
} else if (res.status >= 400 && res.status < 500) {
const body = await res.text();
throw new Error(
throw new DiscordAPIError(
res.status,
body,
`Discord API Error (${res.status}): ${body}`,
);
} else {
Expand All @@ -214,9 +235,15 @@ export class HTTPClient implements HTTPClientOptions {
try {
return await execute();
} catch (error) {
if (error instanceof DiscordAPIError) {
throw error;
}
tries++;
if (nextCause !== undefined) error.cause = nextCause;
if (tries === this.maxRetries) {
if (error instanceof DOMException && error.name === "AbortError") {
throw new DiscordAPITimeoutError(this.timeout, "Request timed out");
}
throw error;
} else {
nextCause = error;
Expand Down

0 comments on commit 84f43e2

Please sign in to comment.