feat: @discordjs/proxy (#7925)

Co-authored-by: Parbez <imranbarbhuiya.fsd@gmail.com>
This commit is contained in:
DD
2022-06-04 14:26:25 +03:00
committed by GitHub
parent e518c8a137
commit 1ba2d2a898
26 changed files with 925 additions and 3 deletions

View File

@@ -0,0 +1,55 @@
import { URL } from 'node:url';
import { DiscordAPIError, HTTPError, RateLimitError, RequestMethod, REST, RouteLike } from '@discordjs/rest';
import {
populateAbortErrorResponse,
populateGeneralErrorResponse,
populateSuccessfulResponse,
populateRatelimitErrorResponse,
} from '../util/responseHelpers';
import type { RequestHandler } from '../util/util';
/**
* Creates an HTTP handler used to forward requests to Discord
* @param rest REST instance to use for the requests
*/
export function proxyRequests(rest: REST): RequestHandler {
return async (req, res) => {
const { method, url } = req;
if (!method || !url) {
throw new TypeError(
'Invalid request. Missing method and/or url, implying that this is not a Server IncomingMesage',
);
}
// The 2nd parameter is here so the URL constructor doesn't complain about an "invalid url" when the origin is missing
// we don't actually care about the origin and the value passed is irrelevant
const fullRoute = new URL(url, 'http://noop').pathname.replace(/^\/api(\/v\d+)?/, '') as RouteLike;
try {
const discordResponse = await rest.raw({
body: req,
fullRoute,
// This type cast is technically incorrect, but we want Discord to throw Method Not Allowed for us
method: method as RequestMethod,
passThroughBody: true,
});
await populateSuccessfulResponse(res, discordResponse);
} catch (error) {
if (error instanceof DiscordAPIError || error instanceof HTTPError) {
populateGeneralErrorResponse(res, error);
} else if (error instanceof RateLimitError) {
populateRatelimitErrorResponse(res, error);
} else if (error instanceof Error && error.name === 'AbortError') {
populateAbortErrorResponse(res);
} else {
// Unclear if there's better course of action here for unknown erorrs. Any web framework allows to pass in an error handler for something like this
// at which point the user could dictate what to do with the error - otherwise we could just 500
throw error;
}
} finally {
res.end();
}
};
}

View File

@@ -0,0 +1,3 @@
export * from './handlers/proxyRequests';
export * from './util/responseHelpers';
export { RequestHandler } from './util/util';

View File

@@ -0,0 +1,54 @@
import type { ServerResponse } from 'node:http';
import { pipeline } from 'node:stream/promises';
import type { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';
import type { Dispatcher } from 'undici';
/**
* Populates a server response with the data from a Discord 2xx REST response
* @param res The server response to populate
* @param data The data to populate the response with
*/
export async function populateSuccessfulResponse(res: ServerResponse, data: Dispatcher.ResponseData): Promise<void> {
res.statusCode = data.statusCode;
for (const header of Object.keys(data.headers)) {
// Strip ratelimit headers
if (header.startsWith('x-ratelimit')) {
continue;
}
res.setHeader(header, data.headers[header]!);
}
await pipeline(data.body, res);
}
/**
* Populates a server response with the data from a Discord non-2xx REST response that is NOT a 429
* @param res The server response to populate
* @param error The error to populate the response with
*/
export function populateGeneralErrorResponse(res: ServerResponse, error: DiscordAPIError | HTTPError): void {
res.statusCode = error.status;
res.statusMessage = error.message;
if ('rawError' in error) {
res.setHeader('Content-Type', 'application/json');
res.write(JSON.stringify(error.rawError));
}
}
/**
* Populates a server response with the data from a Discord 429 REST response
* @param res The server response to populate
* @param error The error to populate the response with
*/
export function populateRatelimitErrorResponse(res: ServerResponse, error: RateLimitError): void {
res.statusCode = 429;
res.setHeader('Retry-After', error.timeToReset / 1000);
}
export function populateAbortErrorResponse(res: ServerResponse): void {
res.statusCode = 504;
res.statusMessage = 'Upstream timed out';
}

View File

@@ -0,0 +1,10 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
/**
* Represents a potentially awaitable value
*/
export type Awaitable<T> = T | PromiseLike<T>;
/**
* Represents a simple HTTP request handler
*/
export type RequestHandler = (req: IncomingMessage, res: ServerResponse) => Awaitable<void>;