Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(proxy): rely on auth header instead #9422

Merged
merged 6 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/proxy-container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

Quickly spin up an instance:

`docker run -d --restart unless-stopped --name proxy -p 127.0.0.1:8080:8080 -e DISCORD_TOKEN=abc discordjs/proxy`
`docker run -d --restart unless-stopped --name proxy -p 127.0.0.1:8080:8080 discordjs/proxy`

Use it:

Expand All @@ -48,6 +48,9 @@ const rest = new REST({
});
```

**Do note that you should not use the same proxy with multiple bots. We cannot guarantee you won't hit rate limits.
Webhooks with tokens or other requests that don't include the Authorization header are okay, though!**

## Links

- [Website][website] ([source][website-source])
Expand Down
6 changes: 1 addition & 5 deletions packages/proxy-container/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import process from 'node:process';
import { proxyRequests } from '@discordjs/proxy';
import { REST } from '@discordjs/rest';

if (!process.env.DISCORD_TOKEN) {
throw new Error('A DISCORD_TOKEN env var is required');
}

// We want to let upstream handle retrying
const api = new REST({ rejectOnRateLimit: () => true, retries: 0 }).setToken(process.env.DISCORD_TOKEN);
const api = new REST({ rejectOnRateLimit: () => true, retries: 0 });
const server = createServer(proxyRequests(api));

const port = Number.parseInt(process.env.PORT ?? '8080', 10);
Expand Down
42 changes: 17 additions & 25 deletions packages/proxy/src/handlers/proxyRequests.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import { URL } from 'node:url';
import {
DiscordAPIError,
HTTPError,
RateLimitError,
type RequestMethod,
type REST,
type RouteLike,
} from '@discordjs/rest';
import {
populateAbortErrorResponse,
populateGeneralErrorResponse,
populateSuccessfulResponse,
populateRatelimitErrorResponse,
} from '../util/responseHelpers.js';
import type { RequestMethod, REST, RouteLike } from '@discordjs/rest';
import { populateSuccessfulResponse, populateErrorResponse } from '../util/responseHelpers.js';
import type { RequestHandler } from '../util/util';

/**
Expand All @@ -36,29 +24,33 @@ export function proxyRequests(rest: REST): RequestHandler {
// eslint-disable-next-line unicorn/no-unsafe-regex, prefer-named-capture-group
const fullRoute = parsedUrl.pathname.replace(/^\/api(\/v\d+)?/, '') as RouteLike;

const headers: Record<string, string> = {
'Content-Type': req.headers['content-type']!,
};

if (req.headers.authorization) {
headers.authorization = req.headers.authorization;
}

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,
// We forward the auth header anyway
auth: false,
passThroughBody: true,
query: parsedUrl.searchParams,
headers: {
'Content-Type': req.headers['content-type']!,
},
headers,
});

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 errors. Any web framework allows to pass in an error handler for something like this
const knownError = populateErrorResponse(res, error);
if (!knownError) {
// Unclear if there's better course of action here for unknown errors.
// 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;
}
Expand Down
23 changes: 22 additions & 1 deletion packages/proxy/src/util/responseHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ServerResponse } from 'node:http';
import { pipeline } from 'node:stream/promises';
import type { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';
import { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';
import type { Dispatcher } from 'undici';

/**
Expand Down Expand Up @@ -59,3 +59,24 @@ export function populateAbortErrorResponse(res: ServerResponse): void {
res.statusCode = 504;
res.statusMessage = 'Upstream timed out';
}

/**
* Tries to populate a server response from an error object
*
* @param res - The server response to populate
* @param error - The error to check and use
* @returns - True if the error is known and the response object was populated, otherwise false
*/
export function populateErrorResponse(res: ServerResponse, error: unknown): boolean {
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 {
return false;
}

return true;
}