cloudflare / cloudflare/workers-sdk

wrangler 4.89.1: container app registration for WfP user workers uses non-WfP API endpoint

Open
#13,936 1 comment 0 reactions 2 assignees Claimed by @th0m View on GitHub
package:wrangler product:containers
Dominant language
TypeScript
Stars
4.5k
Forks
1.5k
Avg merge
3d 8h
Merged PRs (30d)
186

Description

## Summary

`wrangler deploy --dispatch-namespace ` for a Worker with a `containers` declaration successfully uploads the Worker, builds the container image, and pushes the image to `registry.cloudflare.com`. It then fails on the follow-up API call that registers the container application with the Worker, because the call targets the regular Workers Scripts endpoint instead of the dispatch-namespace-scoped endpoint.

This appears to be an incomplete fix from [PR #13824](https://github.com/cloudflare/workers-sdk/pull/13824) (released in 4.89.1). The PR moved `deployContainers()` to run before the WfP early-exit so that container deployment is attempted at all for WfP user workers — but the container-registration request itself was not made WfP-aware.

## Affected versions

- `wrangler@4.89.1` (verified by us)
- `wrangler@4.90.0` — changelog mentions only "Fix Containers SSH config", unrelated. Not retested but no fix referenced.

## What works

- Worker script uploads into the dispatch namespace. `wrangler dispatch-namespace get ` reports `script_count: 1` after the failed deploy.
- The bindings table in wrangler's upload preamble shows the Container DO, D1 binding, and R2 binding are all present.
- The container image builds locally and pushes successfully to `registry.cloudflare.com/{ACCOUNT_ID}/-<class>:<version>`.
- D1 and R2 bindings are reachable at runtime through the dispatch namespace (verified by hitting endpoints in the user-worker that exercise `env.DB` and `env.STORAGE`).

## What fails

After the image push, wrangler exits with:

```
✘ [ERROR] A request to the Cloudflare API
(/accounts/{ACCOUNT_ID}/workers/scripts/smoke-tenant/versions/{VERSION_ID})
failed.

This Worker does not exist on your account. [code: 10007]
```

The 404 is correct from Cloudflare's perspective: there is no `smoke-tenant` script under `/workers/scripts/...` because the Worker was uploaded into a dispatch namespace, so it lives at `/workers/dispatch/namespaces/{NAMESPACE}/scripts/smoke-tenant/...`.

At runtime, the Container DO fails to start with:

```json
{
"ok": false,
"status": 500,
"body": "Failed to start container: There is no container application assigned to this ..."
}
```

This is the runtime fingerprint of the unfinished registration step: image exists in the registry, Worker exists in the dispatch namespace, but no container application links the two for the DO class.

## Reproducer

Minimal `wrangler.jsonc` for a WfP user script with container + D1 + R2:

```jsonc
{
"name": "smoke-tenant",
"main": "src/worker.js",
"compatibility_date": "2026-04-01",
"compatibility_flags": ["nodejs_compat"],
"containers": [
{
"class_name": "SmokeContainer",
"image": "./container/Dockerfile",
"instance_type": "lite",
"max_instances": 2
}
],
"durable_objects": {
"bindings": [
{ "name": "SMOKE_CONTAINER", "class_name": "SmokeContainer" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SmokeContainer"] }
],
"d1_databases": [
{ "binding": "DB", "database_name": "<name>", "database_id": "<uuid>" }
],
"r2_buckets": [
{ "binding": "STORAGE", "bucket_name": "<name>" }
]
}
```

Minimal `src/worker.js`:

```js
import { Container } from "@cloudflare/containers";

export class SmokeContainer extends Container {
defaultPort = 8080;
}

export default {
async fetch(req, env) {
const path = new URL(req.url).pathname;
if (path === "/db") {
const r = await env.DB.prepare(
"INSERT INTO smoke (text) VALUES (?) RETURNING id, text",
).bind("hello-d1").all();
return Response.json({ ok: true, results: r.results });
}
if (path === "/r2") {
const key = `smoke-${Date.now()}.txt`;
await env.STORAGE.put(key, "hello-r2");
const obj = await env.STORAGE.get(key);
return Response.json({ ok: !!obj, key, stored: await obj.text() });
}
if (path === "/container") {
const id = env.SMOKE_CONTAINER.idFromName("singleton");
const stub = env.SMOKE_CONTAINER.get(id);
const r = await stub.fetch(new Request("http://container/"));
return Response.json({ ok: r.ok, status: r.status, body: await r.text() });
}
return new Response("ok");
},
};
```

Minimal `Dockerfile`:

```dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN echo "hello-from-wfp-container" > index.html
EXPOSE 8080
CMD ["python", "-m", "http.server", "8080"]
```

Reproduce:

```sh
wrangler d1 create my-db
wrangler r2 bucket create my-bucket
wrangler dispatch-namespace create my-ns
# substitute IDs into wrangler.jsonc
wrangler deploy --dispatch-namespace my-ns
```

## Suggested fix

When `--dispatch-namespace <ns>` is provided, the container-application registration request (the POST to `/workers/scripts/{name}/versions/{id}`) should be routed to the dispatch-namespace-scoped path:

```
/accounts/{account}/workers/dispatch/namespaces/{ns}/scripts/{name}/versions/{id}
```

instead of:

```
/accounts/{account}/workers/scripts/{name}/versions/{id}
```

## Why this matters

This is the last gap blocking WfP user workers from natively owning their full stack (containers + D1 + R2 + queues + KV + …). With this fix, multi-tenant platforms built on WfP no longer need cross-script Durable Object binding workarounds for containers.

## Local artifacts

A complete reproducer (Makefile, provision/configure/deploy/test scripts, Worker code, Dockerfile, dispatcher Worker) is at `experiments/wfp-native-smoke/` in our repo. Happy to share if useful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.