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

[Miniflare 3] Add outboundService option for customising global fetch() target #629

Merged
merged 1 commit into from
Jul 15, 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
6 changes: 6 additions & 0 deletions packages/miniflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ parameter in module format Workers.
handler. This allows you to access data and functions defined in Node.js
from your Worker.

- `outboundService?: string | { network: Network } | { external: ExternalServer } | { disk: DiskDirectory } | (request: Request) => Awaitable<Response>`

Dispatch this Worker's global `fetch()` and `connect()` requests to the
configured service. Service designators follow the same rules above for
`serviceBindings`.

- `routes?: string[]`

Array of route patterns for this Worker. These follow the same
Expand Down
15 changes: 12 additions & 3 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ import {
normaliseDurableObject,
} from "./plugins";
import {
CUSTOM_SERVICE_KNOWN_OUTBOUND,
CustomServiceKind,
JsonErrorSchema,
NameSourceOptions,
ServiceDesignatorSchema,
getUserServiceName,
handlePrettyErrorRequest,
reviveError,
Expand Down Expand Up @@ -545,9 +548,15 @@ export class Miniflare {
// TODO: technically may want to keep old versions around so can always
// recover this in case of setOptions()?
const workerIndex = parseInt(customService.substring(0, slashIndex));
const serviceName = customService.substring(slashIndex + 1);
const service =
this.#workerOpts[workerIndex]?.core.serviceBindings?.[serviceName];
const serviceKind = customService[slashIndex + 1] as CustomServiceKind;
const serviceName = customService.substring(slashIndex + 2);
let service: z.infer<typeof ServiceDesignatorSchema> | undefined;
if (serviceKind === CustomServiceKind.UNKNOWN) {
service =
this.#workerOpts[workerIndex]?.core.serviceBindings?.[serviceName];
} else if (serviceName === CUSTOM_SERVICE_KNOWN_OUTBOUND) {
service = this.#workerOpts[workerIndex]?.core.outboundService;
}
// Should only define custom service bindings if `service` is a function
assert(typeof service === "function");
try {
Expand Down
22 changes: 19 additions & 3 deletions packages/miniflare/src/plugins/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,28 @@ const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`;
export function getUserServiceName(workerName = "") {
return `${SERVICE_USER_PREFIX}:${workerName}`;
}

// Namespace custom services to avoid conflicts between user-specified names
// and hardcoded Miniflare names
export enum CustomServiceKind {
UNKNOWN = "#", // User specified name (i.e. `serviceBindings`)
KNOWN = "$", // Miniflare specified name (i.e. `outboundService`)
}

export const CUSTOM_SERVICE_KNOWN_OUTBOUND = "outbound";

export function getBuiltinServiceName(
workerIndex: number,
kind: CustomServiceKind,
bindingName: string
) {
return `${SERVICE_BUILTIN_PREFIX}:${workerIndex}:${bindingName}`;
return `${SERVICE_BUILTIN_PREFIX}:${workerIndex}:${kind}${bindingName}`;
}
export function getCustomServiceName(workerIndex: number, bindingName: string) {
return `${SERVICE_CUSTOM_PREFIX}:${workerIndex}:${bindingName}`;

export function getCustomServiceName(
workerIndex: number,
kind: CustomServiceKind,
bindingName: string
) {
return `${SERVICE_CUSTOM_PREFIX}:${workerIndex}:${kind}${bindingName}`;
}
121 changes: 86 additions & 35 deletions packages/miniflare/src/plugins/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SCRIPT_ENTRY from "worker:core/entry";
import { z } from "zod";
import {
Service,
ServiceDesignator,
Worker_Binding,
Worker_Module,
kVoid,
Expand All @@ -31,6 +32,8 @@ import {
parseRoutes,
} from "../shared";
import {
CUSTOM_SERVICE_KNOWN_OUTBOUND,
CustomServiceKind,
SERVICE_ENTRY,
getBuiltinServiceName,
getCustomServiceName,
Expand Down Expand Up @@ -82,6 +85,7 @@ export const CoreOptionsSchema = z.intersection(
textBlobBindings: z.record(z.string()).optional(),
dataBlobBindings: z.record(z.string()).optional(),
serviceBindings: z.record(ServiceDesignatorSchema).optional(),
outboundService: ServiceDesignatorSchema.optional(),

unsafeEphemeralDurableObjects: z.boolean().optional(),
})
Expand Down Expand Up @@ -139,6 +143,57 @@ export const SCRIPT_CUSTOM_SERVICE = `addEventListener("fetch", (event) => {
event.respondWith(${CoreBindings.SERVICE_LOOPBACK}.fetch(request));
})`;

function getCustomServiceDesignator(
workerIndex: number,
kind: CustomServiceKind,
name: string,
service: z.infer<typeof ServiceDesignatorSchema>
): ServiceDesignator {
let serviceName: string;
if (typeof service === "function") {
// Custom `fetch` function
serviceName = getCustomServiceName(workerIndex, kind, name);
} else if (typeof service === "object") {
// Builtin workerd service: network, external, disk
serviceName = getBuiltinServiceName(workerIndex, kind, name);
} else {
// Regular user worker
serviceName = getUserServiceName(service);
}
return { name: serviceName };
}

function maybeGetCustomServiceService(
workerIndex: number,
kind: CustomServiceKind,
name: string,
service: z.infer<typeof ServiceDesignatorSchema>
): Service | undefined {
if (typeof service === "function") {
// Custom `fetch` function
return {
name: getCustomServiceName(workerIndex, kind, name),
worker: {
serviceWorkerScript: SCRIPT_CUSTOM_SERVICE,
compatibilityDate: "2022-09-01",
bindings: [
{
name: CoreBindings.TEXT_CUSTOM_SERVICE,
text: `${workerIndex}/${kind}${name}`,
},
WORKER_BINDING_SERVICE_LOOPBACK,
],
},
};
} else if (typeof service === "object") {
// Builtin workerd service: network, external, disk
return {
name: getBuiltinServiceName(workerIndex, kind, name),
...service,
};
}
}

const FALLBACK_COMPATIBILITY_DATE = "2000-01-01";

function getCurrentCompatibilityDate() {
Expand Down Expand Up @@ -217,20 +272,14 @@ export const CORE_PLUGIN: Plugin<
if (options.serviceBindings !== undefined) {
bindings.push(
...Object.entries(options.serviceBindings).map(([name, service]) => {
let serviceName: string;
if (typeof service === "function") {
// Custom `fetch` function
serviceName = getCustomServiceName(workerIndex, name);
} else if (typeof service === "object") {
// Builtin workerd service: network, external, disk
serviceName = getBuiltinServiceName(workerIndex, name);
} else {
// Regular user worker
serviceName = getUserServiceName(service);
}
return {
name: name,
service: { name: serviceName },
service: getCustomServiceDesignator(
workerIndex,
CustomServiceKind.UNKNOWN,
name,
service
),
};
})
);
Expand Down Expand Up @@ -288,6 +337,15 @@ export const CORE_PLUGIN: Plugin<
: options.unsafeEphemeralDurableObjects
? { inMemory: kVoid }
: { localDisk: DURABLE_OBJECTS_STORAGE_SERVICE_NAME },
globalOutbound:
options.outboundService === undefined
? undefined
: getCustomServiceDesignator(
workerIndex,
CustomServiceKind.KNOWN,
CUSTOM_SERVICE_KNOWN_OUTBOUND,
options.outboundService
),
cacheApiOutbound: { name: getCacheServiceName(workerIndex) },
},
},
Expand All @@ -296,31 +354,24 @@ export const CORE_PLUGIN: Plugin<
// Define custom `fetch` services if set
if (options.serviceBindings !== undefined) {
for (const [name, service] of Object.entries(options.serviceBindings)) {
if (typeof service === "function") {
// Custom `fetch` function
services.push({
name: getCustomServiceName(workerIndex, name),
worker: {
serviceWorkerScript: SCRIPT_CUSTOM_SERVICE,
compatibilityDate: "2022-09-01",
bindings: [
{
name: CoreBindings.TEXT_CUSTOM_SERVICE,
text: `${workerIndex}/${name}`,
},
WORKER_BINDING_SERVICE_LOOPBACK,
],
},
});
} else if (typeof service === "object") {
// Builtin workerd service: network, external, disk
services.push({
name: getBuiltinServiceName(workerIndex, name),
...service,
});
}
const maybeService = maybeGetCustomServiceService(
workerIndex,
CustomServiceKind.UNKNOWN,
name,
service
);
if (maybeService !== undefined) services.push(maybeService);
}
}
if (options.outboundService !== undefined) {
const maybeService = maybeGetCustomServiceService(
workerIndex,
CustomServiceKind.KNOWN,
CUSTOM_SERVICE_KNOWN_OUTBOUND,
options.outboundService
);
if (maybeService !== undefined) services.push(maybeService);
}

return services;
},
Expand Down
41 changes: 41 additions & 0 deletions packages/miniflare/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Miniflare,
MiniflareCoreError,
MiniflareOptions,
Response,
_transformsForContentEncoding,
fetch,
} from "miniflare";
Expand Down Expand Up @@ -328,6 +329,46 @@ test("Miniflare: custom service binding to another Miniflare instance", async (t
});
});

test("Miniflare: custom outbound service", async (t) => {
const mf = new Miniflare({
workers: [
{
name: "a",
modules: true,
script: `export default {
async fetch() {
const res1 = await (await fetch("https://example.com/1")).text();
const res2 = await (await fetch("https://example.com/2")).text();
return Response.json({ res1, res2 });
}
}`,
outboundService: "b",
},
{
name: "b",
modules: true,
script: `export default {
async fetch(request, env) {
if (request.url === "https://example.com/1") {
return new Response("one");
} else {
return fetch(request);
}
}
}`,
outboundService(request) {
return new Response(`fallback:${request.url}`);
},
},
],
});
const res = await mf.dispatchFetch("http://localhost");
t.deepEqual(await res.json(), {
res1: "one",
res2: "fallback:https://example.com/2",
});
});

test("Miniflare: custom upstream as origin", async (t) => {
const upstream = await useServer(t, (req, res) => {
res.end(`upstream: ${new URL(req.url ?? "", "http://upstream")}`);
Expand Down