diff --git a/src/content/changelog/workers/2025-09-26-ctx-exports.md b/src/content/changelog/workers/2025-09-26-ctx-exports.md new file mode 100644 index 000000000000000..ca40c83e6442e7d --- /dev/null +++ b/src/content/changelog/workers/2025-09-26-ctx-exports.md @@ -0,0 +1,30 @@ +--- +title: Automatic loopback bindings via ctx.exports +description: You no longer have to configure bindings explicitly when they point back to your own Worker's top-level exports +date: 2025-09-26 +--- + +The [`ctx.exports` API](/workers/runtime-apis/context/#exports) contains automatically-configured bindings corresponding to your Worker's top-level exports. For each top-level export extending `WorkerEntrypoint`, `ctx.exports` will contain a [Service Binding](/workers/runtime-apis/bindings/service-bindings) by the same name, and for each export extending `DurableObject` (and for which storage has been configured via a [migration](/durable-objects/reference/durable-objects-migrations/)), `ctx.exports` will contain a [Durable Object namespace binding](/durable-objects/api/namespace/). This means you no longer have to configure these bindings explicitly in `wrangler.jsonc`/`wrangler.toml`. + +Example: + +```js +import { WorkerEntrypoint } from "cloudflare:workers"; + +export class Greeter extends WorkerEntrypoint { + greet(name) { + return `Hello, ${name}!`; + } +} + +export default { + async fetch(request, env, ctx) { + let greeting = await ctx.exports.Greeter.greet("World") + return new Response(greeting); + } +} +``` + +At present, you must use [the `enable_ctx_exports` compatibility flag](/workers/configuration/compatibility-flags#enable-ctxexports) to enable this API, though it will be on by default in the future. + +[See the API reference for more information.](/workers/runtime-apis/context/#exports) diff --git a/src/content/compatibility-flags/enable-ctx-exports.md b/src/content/compatibility-flags/enable-ctx-exports.md new file mode 100644 index 000000000000000..e81af56f016d358 --- /dev/null +++ b/src/content/compatibility-flags/enable-ctx-exports.md @@ -0,0 +1,14 @@ +--- +_build: + publishResources: false + render: never + list: never + +name: "Enable ctx.exports" +sort_date: "2025-09-24" +enable_flag: "enable_ctx_exports" +--- + +This flag enables [the `ctx.exports` API](/workers/runtime-apis/context/#exports), which contains automatically-configured loopback bindings for your Worker's top-level exports. This allows you to skip configuring explicit bindings for your `WorkerEntrypoint`s and Durable Object namespaces defined in the same Worker. + +We may change this API to be enabled by default in the future (regardless of compat date or flags). diff --git a/src/content/docs/durable-objects/api/state.mdx b/src/content/docs/durable-objects/api/state.mdx index bb2f6222e78e99f..858d04b764cb38f 100644 --- a/src/content/docs/durable-objects/api/state.mdx +++ b/src/content/docs/durable-objects/api/state.mdx @@ -49,7 +49,11 @@ export class MyDurableObject extends DurableObject { -## Methods +## Methods and Properties + +### `exports` + +Contains loopback bindings to the Worker's own top-level exports. This has exactly the same meaning as [`ExecutionContext`'s `ctx.exports`](/workers/runtime-apis/context/#exports). ### `waitUntil` diff --git a/src/content/docs/workers/runtime-apis/bindings/worker-loader.mdx b/src/content/docs/workers/runtime-apis/bindings/worker-loader.mdx new file mode 100644 index 000000000000000..fc5cdf0b2560c83 --- /dev/null +++ b/src/content/docs/workers/runtime-apis/bindings/worker-loader.mdx @@ -0,0 +1,233 @@ +--- +pcx_content_type: configuration +title: Dynamic Worker Loaders +head: [] +description: The Dynamic Worker Loader API, which allows dynamically spawning isolates that run arbitrary code. + +--- + +import { + Type, + MetaInfo, + WranglerConfig +} from "~/components"; + +:::note[Dynamic Worker Loading is in closed beta] + +The Worker Loader API is available in local development with Wrangler and workerd. But, to use it in production, you must [sign up for the closed beta](https://forms.gle/MoeDxE9wNiqdf8ri9). + +::: + +A Worker Loader binding allows you to load additional Workers containing arbitrary code at runtime. + +An isolate is like a lightweight container. [The Workers platform uses isolates instead of containers or VMs](/workers/reference/how-workers-works/), so every Worker runs in an isolate already. But, a Worker Loader binding allows your Worker to create additional isolates that load arbitrary code on-demand. + +Isolates are much cheaper than containers. You can start an isolate in milliseconds, and it's fine to start one just to run a snippet of code and immediately throw away. There's no need to worry about pooling isolates or trying to reuse already-warm isolates, as you would need to do with containers. + +Worker Loaders also enable **sandboxing** of code, meaning that you can strictly limit what the code is allowed to do. In particular: +* You can arrange to intercept or simply block all network requests made by the Worker within. +* You can supply the sandboxed Worker with custom bindings to represent specific resources which it should be allowed to access. + +With proper sandboxing configured, you can safely run code you do not trust in a dynamic isolate. + +A Worker Loader is a binding with just one method, `get()`, which loads an isolate. Example usage: + +```js +let id = "foo"; + +// Get the isolate with the given ID, creating it if no such isolate exists yet. +let worker = env.LOADER.get(id, async () => { + // If the isolate does not already exist, this callback is invoked to fetch + // the isolate's Worker code. + + return { + compatibilityDate: "2025-06-01", + + // Specify the worker's code (module files). + mainModule: "foo.js", + modules: { + "foo.js": + "export default {\n" + + " fetch(req, env, ctx) { return new Response('Hello'); }\n" + + "}\n", + }, + + // Specify the dynamic Worker's environment (`env`). This is specified + // as a JavaScript object, exactly as you want it to appear to the + // child Worker. It can contain basic serializable types as well as + // Service Bindings (see below). + env: { + SOME_ENV_VAR: 123 + }, + + // To block the worker from talking to the internet using `fetch()` or + // `connect()`, set `globalOutbound` to `null`. You can also set this + // to any service binding, to have calls be intercepted and redirected + // to that binding. + globalOutbound: null, + }; +}); + +// Now you can get the Worker's entrypoint and send requests to it. +let defaultEntrypoint = worker.getEntrypoint(); +await defaultEntrypoint.fetch("http://example.com"); + +// You can get non-default entrypoints as well, and specify the +// `ctx.props` value to be delivered to the entrypoint. +let someEntrypoint = worker.getEntrypoint("SomeEntrypointClass", { + props: {someProp: 123} +}); +``` + +## Configuration + +To add a dynamic worker loader binding to your worker, add it to your Wrangler config like so: + + + +```toml +[[worker_loaders]] +binding = "LOADER" +``` + + + +## API Reference + +### `get` + +get(id , getCodeCallback ): + +Loads a Worker with the given ID, returning a `WorkerStub` which may be used to invoke the Worker. + +As a convenience, the loader implements caching of isolates. When a new ID is seen the first time, a new isolate is loaded. But, the isolate may be kept warm in memory for a while. If later invocations of the loader request the same ID, the existing isolate may be returned again, rather than create a new one. But there is no guarantee: a later call with the same ID may instead start a new isolate from scratch. + +Whenever the system determines it needs to start a new isolate, and it does not already have a copy of the code cached, it will invoke `codeCallback` to get the Worker's code. This is an async callback, so the application can load the code from remote storage if desired. The callback returns a `WorkerCode` object (described below). + +Because of the caching, you should ensure that the callback always returns exactly the same content, when called for the same ID. If anything about the content changes, you must use a new ID. But if the content hasn't changed, it's best to reuse the same ID in order to take advantage of caching. If the `WorkerCode` is different every time, you can pass a random ID. + +You could, for example, use IDs of the form `:`, where the version number increments every time the code changes. Or, you could compute IDs based on a hash of the code and config, so that any change results in a new ID. + +`get()` returns a `WorkerStub`, which can be used to send requests to the loaded Worker. Note that the stub is returned synchronously—you do not have to await it. If the Worker is not loaded yet, requests made to the stub will wait for the Worker to load before being delivered. If loading fails, the request will throw an exception. + +It is never guaranteed that two requests will go to the same isolate. Even if you use the same `WorkerStub` to make multiple requests, they could execute in different isolates. The callback passed to `loader.get()` could be called any number of times (although it is unusual for it to be called more than once). + +### `WorkerCode` + +This is the structure returned by `getCodeCallback` to represent a worker. + +#### compatibilityDate + +The [compatibility date](/workers/configuration/compatibility-dates/) for the Worker. This has the same meaning as the `compatibility_date` setting in a Wrangler config file. + +#### compatibilityFlags + +An optional list of [compatibility flags](/workers/configuration/compatibility-flags) augmenting the compatibility date. This has the same meaning as the `compatibility_flags` setting in a Wrangler config file. + +#### allowExperimental + +If true, then experimental compatibility flags will be permitted in `compatibilityFlags`. In order to set this, the worker calling the loader must itself have the compatibility flag `"experimental"` set. Experimental flags cannot be enabled in production. + +#### mainModule + +The name of the Worker's main module. This must be one of the modules listed in `modules`. + +#### modules + +A dictionary object mapping module names to their string contents. If the module content is a plain string, then the module name must have a file extension indicating its type: either `.js` or `.py`. + +A module's content can also be specified as an object, in order to specify its type independent from the name. The allowed objects are: + +* `{js: string}`: A JavaScript module, using ES modules syntax for imports and exports. +* `{cjs: string}`: A CommonJS module, using `require()` syntax for imports. +* `{py: string}`: A [Python module](/workers/languages/python/), but see the warning below. +* `{text: string}`: An importable string value. +* `{data: ArrayBuffer}`: An importable `ArrayBuffer` value. +* `{json: object}`: An importable object. The value must be JSON-serializable. However, note that value is provided as a parsed object, and is delivered as a parsed object; neither side actually sees the JSON serialization. + +:::caution[Warning] + +While Dynamic Isolates support Python, please note that at this time, Python Workers are much slower to start than JavaScript Workers, which may defeat some of the benefits of dynamic isolate loading. They may also be priced differently, when Worker Loaders become generally available. + +::: + +#### globalOutbound Optional + +Controls whether the dynamic Worker has access to the network. The global `fetch()` and `connect()` functions (for making HTTP requests and TCP connections, respectively) can be blocked or redirected to isolate the Worker. + +If `globalOutbound` is not specified, the default is to inherit the parent's network access, which usually means the dynamic Worker will have full access to the public Internet. + +If `globalOutbound` is `null`, then the dynamic Worker will be totally cut off from the network. Both `fetch()` and `connect()` will throw exceptions. + +`globalOutbound` can also be set to any service binding, including service bindings in the parent worker's `env` as well as [loopback bindings from `ctx.exports`](/workers/runtime-apis/context/#exports). + +Using `ctx.exports` is particularly useful as it allows you to customize the binding further for the specific sandbox, by setting the value of `ctx.props` that should be passed back to it. The `props` can contain information to identify the specific dynamic Worker that made the request. + +For example: + +```js +import { WorkerEntrypoint } from "cloudflare:workers"; + +export class Greeter extends WorkerEntrypoint { + fetch(request) { + return new Response(`Hello, ${this.ctx.props.name}!`); + } +} + +export default { + async fetch(request, env, ctx) { + let worker = env.LOADER.get("alice", () => { + return { + // Redirect the worker's global outbound to send all requests + // to the `Greeter` class, filling in `ctx.props.name` with + // the name "Alice", so that it always responds "Hello, Alice!". + globalOutbound: ctx.exports.Greeter({props: {name: "Alice"}}) + + // ... code ... + } + }); + } +} +``` + +#### env + +The environment object to provide to the dynamic Worker. + +Using this, you can provide custom bindings to the Worker. + +`env` is serialized and transferred into the dynamic Worker, where it is used directly as the value of `env` there. It may contain: + +* [Structured clonable types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). +* [Service Bindings](/workers/runtime-apis/bindings/service-bindings), including [loopback bindings from `ctx.exports`](/workers/runtime-apis/context/#exports). + +The second point is the key to creating custom bindings: you can define a binding with any arbitrary API, by defining a [`WorkerEntrypoint` class](/workers/runtime-apis/bindings/service-bindings/rpc) implementing an RPC API, and then giving it to the dynamic Worker as a Service Binding. + +Moreover, by using `ctx.exports` loopback bindings, you can further customize the bindings for the specific dynamic Worker by setting `ctx.props`, just as described for `globalOutbound`, above. + +```js +import { WorkerEntrypoint } from "cloudflare:workers"; + +// Implement a binding which can be called by the dynamic Worker. +export class Greeter extends WorkerEntrypoint { + greet() { + return `Hello, ${this.ctx.props.name}!`; + } +} + +export default { + async fetch(request, env, ctx) { + let worker = env.LOADER.get("alice", () => { + return { + env: { + // Provide a binding which has a method greet() which can be called + // to receive a greeting. The binding knows the Worker's name. + GREETER: ctx.exports.Greeter({props: {name: "Alice"}}) + } + + // ... code ... + } + }); + } +} +``` diff --git a/src/content/docs/workers/runtime-apis/context.mdx b/src/content/docs/workers/runtime-apis/context.mdx index b85b5407018e644..8c6794f88437782 100644 --- a/src/content/docs/workers/runtime-apis/context.mdx +++ b/src/content/docs/workers/runtime-apis/context.mdx @@ -2,17 +2,167 @@ pcx_content_type: configuration title: Context (ctx) head: [] -description: The Context API in Cloudflare Workers, including waitUntil and +description: The Context API in Cloudflare Workers, including props, exports, waitUntil and passThroughOnException. --- +import { + Tabs, + TabItem, + WranglerConfig +} from "~/components"; + The Context API provides methods to manage the lifecycle of your Worker or Durable Object. Context is exposed via the following places: * As the third parameter in all [handlers](/workers/runtime-apis/handlers/), including the [`fetch()` handler](/workers/runtime-apis/handlers/fetch/). (`fetch(request, env, ctx)`) -* As a class property of the [`WorkerEntrypoint` class](/workers/runtime-apis/bindings/service-bindings/rpc) +* As a class property of the [`WorkerEntrypoint` class](/workers/runtime-apis/bindings/service-bindings/rpc) (`this.ctx`) + +Note that the Context API is available strictly in stateless contexts, that is, not [Durable Objects](/durable-objects/). However, Durable Objects have a different object, the [Durable Object State](/durable-objects/api/state/), which is available as `this.ctx` inside a Durable Object class, and provides some of the same functionality as the Context API. + +## `props` + +`ctx.props` provides a way to pass additional configuration to a worker based on the context in which it was invoked. For example, when your Worker is called by another Worker, `ctx.props` can provide information about the calling worker. + +For example, imagine that you are configuring a Worker called "frontend-worker", which must talk to another Worker called "doc-worker" in order to manipulate documents. You might configure "frontend-worker" with a [Service Binding](/workers/runtime-apis/bindings/service-bindings) like: + + + +```toml +[[services]] +binding = "DOC_SERVICE" +service = "doc-worker" +entrypoint = "DocServiceApi" +props = { clientId = "frontend-worker", permissions = ["read", "write"] } +``` + + + +Now frontend-worker can make calls to doc-worker with code like `env.DOC_SERVICE.getDoc(id)`. This will make a [Remote Procedure Call](/workers/runtime-apis/rpc/) invoking the method `getDoc()` of the class `DocServiceApi`, a [`WorkerEntrypoint` class](/workers/runtime-apis/bindings/service-bindings/rpc) exported by doc-worker. + +The configuration contains a `props` value. This in an arbitrary JSON value. When the `DOC_SERVICE` binding is used, the `DocServiceApi` instance receiving the call will be able to access this `props` value as `this.ctx.props`. Here, we've configured `props` to specify that the call comes from frontend-worker, and that it should be allowed to read and write documents. However, the contents of `props` can be anything you want. + +The Workers platform is designed to ensure that `ctx.props` can only be set by someone who has permission to edit and deploy the worker to which it is being delivered. This means that you can trust that the content of `ctx.props` is authentic. There is no need to use secret keys or cryptographic signatures in a `ctx.props` value. + +`ctx.props` can also be used to configure an RPC interface to represent a _specific_ resource, thus creating a "custom binding". For example, we could configure a Service Binding to our "doc-worker" which grants access only to a specific document: + + + +```toml +[[services]] +binding = "FOO_DOCUMENT" +service = "doc-worker" +entrypoint = "DocumentApi" +props = { docId = "e366592caec1d88dff724f74136b58b5", permissions = ["read", "write"] } +``` + + + +Here, we've placed a `docId` property in `ctx.props`. The `DocumentApi` class could be designed to provide an API to the specific document identified by `ctx.props.docId`, and enforcing the given permissions. + +## `exports` + +:::note[Compatibility flag required] + +To use `ctx.exports`, you must use [the `enable_ctx_exports` compatibility flag](/workers/configuration/compatibility-flags#enable-ctxexports). + +::: + +`ctx.exports` provides automatically-configured "loopback" bindings for all of your top-level exports. + +* For each top-level export that `extends WorkerEntrypoint` (or simply implements a fetch handler), `ctx.exports` automatically contains a [Service Binding](/workers/runtime-apis/bindings/service-bindings). +* For each top-level export that `extends DurableObject` (and which has been configured with storage via a [migration](/durable-objects/reference/durable-objects-migrations/)), `ctx.exports` automatically contains a [Durable Object namespace binding](/durable-objects/api/namespace/). + +For example: + +```js +import { WorkerEntrypoint } from "cloudflare:workers"; + +export class Greeter extends WorkerEntrypoint { + greet(name) { + return `Hello, ${name}!`; + } +} + +export default { + async fetch(request, env, ctx) { + let greeting = await ctx.exports.Greeter.greet("World") + return new Response(greeting); + } +} +``` + +In this example, the default fetch handler calls the `Greeter` class over RPC, like how you'd use a Service Binding. However, there is no external configuration required. `ctx.exports` is populated _automatically_ from your top-level imports. + +### Specifying `ctx.props` when using `ctx.exports` + +Loopback Service Bindings in `ctx.exports` have an extra capability that regular Service Bindings do not: the caller can specify the value of `ctx.props` that should be delivered to the callee. + + + +```js +import { WorkerEntrypoint } from "cloudflare:workers"; + +export class Greeter extends WorkerEntrypoint { + greet(name) { + return `${this.ctx.props.greeting}, ${name}!`; + } +} + +export default { + async fetch(request, env, ctx) { + // Make a custom greeter that uses the greeting "Welcome". + let greeter = ctx.exports.Greeter({props: {greeting: "Welcome"}}) + + // Greet the world. Returns "Welcome, World!" + let greeting = await ctx.exports.Greeter.greet("World") + + return new Response(greeting); + } +} +``` + + + +```ts +import { WorkerEntrypoint } from "cloudflare:workers"; + +type Props = { + greeting: string +} + +export class Greeter extends WorkerEntrypoint { + greet(name) { + return `${this.ctx.props.greeting}, ${name}!`; + } +} + +export default { + async fetch(request, env, ctx) { + // Make a custom greeter that uses the greeting "Welcome". + let greeter = ctx.exports.Greeter({props: {greeting: "Welcome"}}) + + // Greet the world. Returns "Welcome, World!" + let greeting = await ctx.exports.Greeter.greet("World") + + return new Response(greeting); + } +} satisfies ExportedHandler; +``` + + + +Specifying props dynamically is permitted in this case because the caller is the same Worker, and thus can be presumed to be trusted to specify any props. The ability to customize props is particularly useful when the resulting binding is to be passed to another Worker over RPC or used in the `env` of a [dynamically-loaded worker](/workers/runtime-apis/bindings/worker-loader/). + +Note that `props` values specified in this way are allowed to contain any "persistently" serializable type. This includes all basic [structured clonable data types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). It also includes Service Bindings themselves: you can place a Service Binding into the `props` of another Service Binding. + +### TypeScript types for `ctx.exports` and `ctx.props` + +If using TypeScript, you should use [the `wrangler types` command](/workers/wrangler/commands/#types) to auto-generate types for your project. The generated types will ensure `ctx.exports` is typed correctly. + +When declaring an entrypoint class that accepts `props`, make sure to declare it as `extends WorkerEntrypoint`, where `Props` is the type of `ctx.props`. See the example above. ## `waitUntil`