/// // Copied from https://github.com/cloudflare/workers-sdk/blob/97a72ccd6ccf57b0c6c62566e638666ea1f0cf71/packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts import { newWorkersRpcResponse } from "capnweb"; import { EmailMessage } from "cloudflare:email"; interface Env extends Record {} class BindingNotFoundError extends Error { constructor(name?: string) { super(`Binding ${name ? `"${name}"` : ""} not found`); } } /** * For most bindings, we expose them as * - RPC stubs directly to capnweb, or * - HTTP based fetchers * However, there are some special cases: * - SendEmail bindings need to take EmailMessage as their first parameter, * which is not serialisable. As such, we reconstruct it before sending it * on to the binding. See packages/miniflare/src/workers/email/email.worker.ts * - Dispatch Namespace bindings have a synchronous .get() method. Since we * can't emulate that over an async boundary, we mock it locally and _actually_ * perform the .get() remotely at the first appropriate async point. See * packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace.worker.ts * * getExposedJSRPCBinding() and getExposedFetcher() perform the logic for figuring out * which binding is being accessed, dependending on the request. Note: Both have logic * for dispatch namespaces, because dispatch namespaces can use both fetch or RPC depending * on context. */ function getExposedJSRPCBinding(request: Request, env: Env) { const url = new URL(request.url); const bindingName = url.searchParams.get("MF-Binding"); if (!bindingName) { throw new BindingNotFoundError(); } const targetBinding = env[bindingName]; if (!targetBinding) { throw new BindingNotFoundError(bindingName); } if (targetBinding.constructor.name === "SendEmail") { return { async send( value: ForwardableEmailMessage | Parameters[0], ) { const raw = value && typeof value === "object" && "EmailMessage::raw" in value ? value["EmailMessage::raw"] : undefined; if ( value && typeof value === "object" && "from" in value && "to" in value && (typeof raw === "string" || raw instanceof ReadableStream) ) { const message = new EmailMessage(value.from, value.to, raw); return (targetBinding as SendEmail).send(message); } return (targetBinding as SendEmail).send( value as Parameters[0], ); }, }; } if (url.searchParams.has("MF-Dispatch-Namespace-Options")) { const { name, args, options } = JSON.parse( url.searchParams.get("MF-Dispatch-Namespace-Options")!, ); return (targetBinding as DispatchNamespace).get(name, args, options); } return targetBinding; } function getExposedFetcher(request: Request, env: Env) { const bindingName = request.headers.get("MF-Binding"); if (!bindingName) { throw new BindingNotFoundError(); } const targetBinding = env[bindingName]; if (!targetBinding) { throw new BindingNotFoundError(bindingName); } // Special case the Dispatch Namespace binding because it has a top-level synchronous .get() call const dispatchNamespaceOptions = request.headers.get( "MF-Dispatch-Namespace-Options", ); if (dispatchNamespaceOptions) { const { name, args, options } = JSON.parse(dispatchNamespaceOptions); return (targetBinding as DispatchNamespace).get(name, args, options); } return targetBinding as Fetcher; } /** * This Worker can proxy two types of remote binding: * 1. "raw" bindings, where this Worker has been configured to pass through the raw * fetch from a local workerd instance to the relevant binding * 2. JSRPC bindings, where this Worker uses capnweb to proxy RPC * communication in userland. This is always over a WebSocket connection */ function isJSRPCBinding(request: Request): boolean { const url = new URL(request.url); return request.headers.has("Upgrade") && url.searchParams.has("MF-Binding"); } export default { async fetch(request, env) { try { if (isJSRPCBinding(request)) { return newWorkersRpcResponse( request, getExposedJSRPCBinding(request, env), ); } else { const fetcher = getExposedFetcher(request, env); const originalHeaders = new Headers(); for (const [name, value] of request.headers) { if (name.startsWith("mf-header-")) { originalHeaders.set(name.slice("mf-header-".length), value); } else if (name === "upgrade") { // The `Upgrade` header needs to be special-cased to prevent: // TypeError: Worker tried to return a WebSocket in a response to a request which did not contain the header "Upgrade: websocket" originalHeaders.set(name, value); } } return fetcher.fetch( request.headers.get("MF-URL") ?? "http://example.com", new Request(request, { redirect: "manual", headers: originalHeaders, }), ); } } catch (e) { if (e instanceof BindingNotFoundError) { return new Response(e.message, { status: 400 }); } return new Response((e as Error).message, { status: 500 }); } }, } satisfies ExportedHandler;