From 4792ad2721097f9e27ec704ffdeb29651cca12cb Mon Sep 17 00:00:00 2001 From: Fleny Date: Mon, 4 Aug 2025 19:49:44 +0200 Subject: [PATCH] feat(rest): Add events (#4245) * feat(rest): Add events * Apply suggestions from code review Co-authored-by: Awesome Stickz * Remove timeTook, consolidate event types * Update packages/rest/src/manager.ts Co-authored-by: Awesome Stickz * Fix type error * Apply suggestions from code review Co-authored-by: Link --------- Co-authored-by: Awesome Stickz Co-authored-by: Link --- packages/rest/src/manager.ts | 57 ++++++++++++++++++++++++++---------- packages/rest/src/types.ts | 33 ++++++++++++++++++++- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 50d34ee3e..16d5d4360 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -119,6 +119,12 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage token: options.token, version: options.version ?? DISCORD_API_VERSION, logger: options.logger ?? logger, + events: { + request: () => {}, + response: () => {}, + requestError: () => {}, + ...options.events, + }, routes: createRoutes(), @@ -440,10 +446,16 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage loggingHeaders.authorization = `${authorizationScheme} tokenhere` } + const request = new Request(url, payload) + rest.events.request(request, { + body: options.requestBodyOptions?.body, + }) + rest.logger.debug(`sending request to ${url}`, 'with payload:', { ...payload, headers: loggingHeaders }) - const response = await fetch(url, payload).catch(async (error) => { + const response = await fetch(request).catch(async (error) => { rest.logger.error(error) - // Mark request and completed + rest.events.requestError(request, error, { body: options.requestBodyOptions?.body }) + // Mark request as completed rest.invalidBucket.handleCompletedRequest(999, false) options.reject({ ok: false, @@ -454,7 +466,15 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }) rest.logger.debug(`request fetched from ${url} with status ${response.status} & ${response.statusText}`) - // Mark request and completed + // Sometimes the Content-Type may be "application/json; charset=utf-8", for this reason, we need to check the start of the header + const body = await (response.headers.get('Content-Type')?.startsWith('application/json') ? response.json() : response.text()).catch(() => null) + + rest.events.response(request, response, { + requestBody: options.requestBodyOptions?.body, + responseBody: body, + }) + + // Mark request as completed rest.invalidBucket.handleCompletedRequest(response.status, response.headers.get(RATE_LIMIT_SCOPE_HEADER) === 'shared') // Set the bucket id if it was available on the headers @@ -466,15 +486,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage rest.logger.debug(`Request to ${url} failed.`) if (response.status !== HttpResponseCode.TooManyRequests) { - const body = response.headers.get('Content-Type') === 'application/json' ? ((await response.json()) as object) : await response.text() - options.reject({ ok: false, status: response.status, statusText: response.statusText, body }) return } - // Consume the response body to avoid leaking memory - await response.arrayBuffer() - rest.logger.debug(`Request to ${url} was ratelimited.`) // Too many attempts, get rid of request from queue. if (options.retryCount >= rest.maxRetryCount) { @@ -498,8 +513,8 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return await options.retryRequest?.(options) } - // Discord sometimes sends no response with no content. - options.resolve({ ok: true, status: response.status, body: response.status === HttpResponseCode.NoContent ? undefined : await response.text() }) + // Discord sometimes sends a response with no content + options.resolve({ ok: true, status: response.status, body: response.status === HttpResponseCode.NoContent ? undefined : body }) }, simplifyUrl(url, method) { @@ -569,12 +584,22 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage options.headers[rest.authorizationHeader] = rest.authorization } - const result = await fetch(`${rest.baseUrl}/v${rest.version}${route}`, rest.createRequestBody(method, options)) + const request = new Request(`${rest.baseUrl}/v${rest.version}${route}`, rest.createRequestBody(method, options)) + rest.events.request(request, { + body: options?.body, + }) + + const result = await fetch(request) + + // Sometimes the Content-Type may be "application/json; charset=utf-8", for this reason, we need to check the start of the header + const body = await (result.headers.get('Content-Type')?.startsWith('application/json') ? result.json() : result.text()).catch(() => null) + + rest.events.response(request, result, { + requestBody: options?.body, + responseBody: body, + }) if (!result.ok) { - // Sometime the Content-Type may be "application/json; charset=utf-8", for this reason we need to check the start of the header - const body = await (result.headers.get('Content-Type')?.startsWith('application/json') ? result.json() : result.text()).catch(() => null) - error.cause = Object.assign(Object.create(baseErrorPrototype), { ok: false, status: result.status, @@ -584,7 +609,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage throw error } - return result.status !== 204 ? await result.json() : undefined + return result.status !== 204 ? (typeof body === 'string' ? JSON.parse(body) : body) : undefined } return await new Promise(async (resolve, reject) => { @@ -597,7 +622,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage await rest.processRequest(payload) }, resolve: (data) => { - resolve(data.status !== 204 ? JSON.parse(data.body ?? '{}') : undefined) + resolve(data.status !== 204 ? (typeof data.body === 'string' ? JSON.parse(data.body) : data.body) : undefined) }, reject: (reason) => { let errorText: string diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 6f1407b1c..b03b6ff6a 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -196,6 +196,8 @@ export interface CreateRestManagerOptions { * @default logger // The logger exported by `@discordeno/utils` */ logger?: Pick + /** Events for the rest manager */ + events?: Partial } export interface RestManager { @@ -241,6 +243,8 @@ export interface RestManager { routes: RestRoutes /** The logger to use for the rest manager */ logger: Pick + /** Events for the rest manager */ + events: RestManagerEvents /** Allows the user to inject custom headers that will be sent with every request. */ createBaseHeaders: () => Record /** Whether or not the rest manager should keep objects in raw snake case from discord. */ @@ -3251,7 +3255,8 @@ export interface RestRateLimitedPath { export interface RestRequestResponse { ok: boolean status: number - body?: string + /** The returned body parsed if it was JSON, otherwise it will be the raw body as a string */ + body?: string | object } export interface RestRequestRejection { @@ -3263,3 +3268,29 @@ export interface RestRequestRejection { body?: string | object error?: string } + +export interface RestManagerEvents { + /** + * Emitted when a request is made to the API. + * + * @remarks + * The body that will be sent to the API is available in the `extra` parameter. Do not consume the body in the `Request` object and use the one in the `extra` parameter instead. + */ + request: (request: Request, extra: { body: any }) => void + /** + * Emitted when a response is received from the API. + * + * @remarks + * This is fired for both successful and failed requests, you should check the Response object to determine if the request was successful or not. + * + * Both the request and the response body are available in the `extra` parameter. Do not consume the body in the `Request` or `Response` object and use the one in the `extra` parameter instead. + */ + response: (request: Request, response: Response, extra: { requestBody: any; responseBody: string | object }) => void + /** + * Emitted when a request errors due to fetch error. + * + * @remarks + * The body that was sent to the API is available in the `extra` parameter. + */ + requestError: (request: Request, error: any, extra: { body: any }) => void +}