diff --git a/src/backend-emulator/main.js b/src/backend-emulator/main.js index ba793f0..3e30407 100644 --- a/src/backend-emulator/main.js +++ b/src/backend-emulator/main.js @@ -21,6 +21,24 @@ const workerShims = { let listeners = []; +let isResponseConstructorAllowed = false; + +/** + * SafeResponse wraps a fetch Response to yell loudly if constructed at an unsafe time. + * Cloudflare will reject all Response objects that aren't created during a request, so no pre-generation is allowed. + */ +class SafeResponse extends fetch.Response { + constructor(...args) { + super(...args); + + if (!isResponseConstructorAllowed) { + throw new Error( + 'Response object created outside of request context. This will be rejected by Cloudflare.' + ); + } + } +} + const context = () => vm.createContext( { @@ -30,7 +48,7 @@ const context = () => listeners.push(fn); } }, - Response: fetch.Response, + Response: SafeResponse, URL: URL, crypto: crypto, setTimeout: setTimeout, @@ -75,6 +93,7 @@ const server = http.createServer((req, res) => { console.log( `${loggedStatus} [${timeEnd - timeStart}ms] - ${req.method} ${req.url}` ); + isResponseConstructorAllowed = false; }, request: new fetch.Request( new URL(`http://${req.headers.host || 'localhost'}${req.url}`), @@ -95,6 +114,7 @@ const server = http.createServer((req, res) => { return; } + isResponseConstructorAllowed = true; for (let listener of listeners) { try { listener(event); diff --git a/src/backend-worker/handlers/get-picker-data.ts b/src/backend-worker/handlers/get-picker-data.ts index ebd023a..c1f8375 100644 --- a/src/backend-worker/handlers/get-picker-data.ts +++ b/src/backend-worker/handlers/get-picker-data.ts @@ -7,14 +7,15 @@ import { import { respond, withSession } from '../utils/api-tools'; import { getGuild, getGuildData, getGuildMemberRoles } from '../utils/guild'; -const fail = respond( - { - error: 'guild not found', - }, - { - status: 404, - } -); +const fail = () => + respond( + { + error: 'guild not found', + }, + { + status: 404, + } + ); export const GetPickerData = withSession( (session?: SessionData) => async (request: Request): Promise => { @@ -38,14 +39,14 @@ export const GetPickerData = withSession( // Save a Discord API request by checking if this user is a member by session first const checkGuild = guilds.find((guild) => guild.id === guildID); if (!checkGuild) { - return fail; + return fail(); } const guild = await getGuild(guildID, { skipCachePull: url.searchParams.has('__no_cache'), }); if (!guild) { - return fail; + return fail(); } const memberRolesP = getGuildMemberRoles({ @@ -57,7 +58,7 @@ export const GetPickerData = withSession( const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]); if (!memberRoles) { - return fail; + return fail(); } const presentableGuild: PresentableGuild = { diff --git a/src/backend-worker/handlers/login-callback.ts b/src/backend-worker/handlers/login-callback.ts index 1d321ce..0861c18 100644 --- a/src/backend-worker/handlers/login-callback.ts +++ b/src/backend-worker/handlers/login-callback.ts @@ -22,7 +22,7 @@ const AuthErrorResponse = (extra?: string) => ); export const LoginCallback = resolveFailures( - AuthErrorResponse(), + AuthErrorResponse, async (request: Request): Promise => { const query = new URL(request.url).searchParams; diff --git a/src/backend-worker/router.ts b/src/backend-worker/router.ts index 36cd96c..4528a06 100644 --- a/src/backend-worker/router.ts +++ b/src/backend-worker/router.ts @@ -34,7 +34,7 @@ export class Router { this.routingTree[lowerMethod][rootPath] = handler; } - async handle(request: Request): Promise | Response { + async handle(request: Request): Promise { const url = new URL(request.url); if (url.pathname === '/' || url.pathname === '') { diff --git a/src/backend-worker/utils/api-tools.ts b/src/backend-worker/utils/api-tools.ts index bd125ab..e5eb8de 100644 --- a/src/backend-worker/utils/api-tools.ts +++ b/src/backend-worker/utils/api-tools.ts @@ -16,14 +16,16 @@ export const respond = (obj: Record, init?: ResponseInit) => new Response(JSON.stringify(obj), init); export const resolveFailures = ( - handleWith: Response, + handleWith: () => Response, handler: (request: Request) => Promise | Response ) => async (request: Request): Promise => { try { return handler(request); } catch (e) { console.error(e); - return handleWith || respond({ error: 'internal server error' }, { status: 500 }); + return ( + handleWith() || respond({ error: 'internal server error' }, { status: 500 }) + ); } }; @@ -109,7 +111,7 @@ export const cacheLayer = ( const NotAuthenticated = (extra?: string) => respond( { - err: extra || 'not authenticated', + error: extra || 'not authenticated', }, { status: 403 } );