import { Request } from "express"; import { Key } from "../../../shared/key-management"; import { assertNever } from "../../../shared/utils"; /** * Represents a change to the request that will be reverted if the request * fails. */ interface ProxyReqMutation { target: "header" | "path" | "body" | "api-key" | "signed-request"; key?: string; originalValue: any | undefined; } /** * Manages a request's headers, body, and path, allowing them to be modified * before the request is proxied and automatically reverted if the request * needs to be retried. */ export class ProxyReqManager { private req: Request; private mutations: ProxyReqMutation[] = []; /** * A read-only proxy of the request object. Avoid changing any properties * here as they will persist across retries. */ public readonly request: Readonly; constructor(req: Request) { this.req = req; this.request = new Proxy(req, { get: (target, prop) => { if (typeof prop === "string") return target[prop as keyof Request]; return undefined; }, }); } setHeader(name: string, newValue: string): void { const originalValue = this.req.get(name); this.mutations.push({ target: "header", key: name, originalValue }); this.req.headers[name.toLowerCase()] = newValue; } removeHeader(name: string): void { const originalValue = this.req.get(name); this.mutations.push({ target: "header", key: name, originalValue }); delete this.req.headers[name.toLowerCase()]; } setBody(newBody: any): void { const originalValue = this.req.body; this.mutations.push({ target: "body", key: "body", originalValue }); this.req.body = newBody; } setKey(newKey: Key): void { const originalValue = this.req.key; this.mutations.push({ target: "api-key", key: "key", originalValue }); this.req.key = newKey; } setPath(newPath: string): void { const originalValue = this.req.path; this.mutations.push({ target: "path", key: "path", originalValue }); this.req.url = newPath; } setSignedRequest(newSignedRequest: typeof this.req.signedRequest): void { const originalValue = this.req.signedRequest; this.mutations.push({ target: "signed-request", key: "signedRequest", originalValue }); this.req.signedRequest = newSignedRequest; } hasChanged(): boolean { return this.mutations.length > 0; } revert(): void { for (const mutation of this.mutations.reverse()) { switch (mutation.target) { case "header": if (mutation.originalValue === undefined) { delete this.req.headers[mutation.key!.toLowerCase()]; continue; } else { this.req.headers[mutation.key!.toLowerCase()] = mutation.originalValue; } break; case "path": this.req.url = mutation.originalValue; break; case "body": this.req.body = mutation.originalValue; break; case "api-key": // We don't reset the key here because it's not a property of the // inbound request, so we'd only ever be reverting it to null. break; case "signed-request": this.req.signedRequest = mutation.originalValue; break; default: assertNever(mutation.target); } } this.mutations = []; } }