From 695b043b4ddc99bf9a3fe93cc7daa8347b29ccb3 Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Tue, 13 Jan 2026 15:22:15 -0500 Subject: [PATCH 1/2] Improve 'wrangler secret put' error message when using Worker versions (#11882) --- .changeset/ten-pants-wash.md | 5 +++++ packages/wrangler/src/__tests__/secret.test.ts | 9 ++++++--- packages/wrangler/src/secret/index.ts | 11 ++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 .changeset/ten-pants-wash.md diff --git a/.changeset/ten-pants-wash.md b/.changeset/ten-pants-wash.md new file mode 100644 index 000000000000..4d06f6ba2bfc --- /dev/null +++ b/.changeset/ten-pants-wash.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Improve the error message for `wrangler secret put` when using Worker versions or gradual deployments. `wrangler versions secret put` should be used instead, or ensure to deploy the latest version before using `wrangler secret put`. `wrangler secret put` alone will add the new secret to the latest version (possibly undeployed) and immediately deploy that which is usually not intended. diff --git a/packages/wrangler/src/__tests__/secret.test.ts b/packages/wrangler/src/__tests__/secret.test.ts index b02a3b90ae70..6350988fc314 100644 --- a/packages/wrangler/src/__tests__/secret.test.ts +++ b/packages/wrangler/src/__tests__/secret.test.ts @@ -547,9 +547,12 @@ describe("wrangler secret", () => { await expect(runWrangler(`secret put secret-name --name ${scriptName}`)) .rejects.toThrowErrorMatchingInlineSnapshot(` - [Error: Secret edit failed. You attempted to modify a secret, but the latest version of your Worker isn't currently deployed. Please ensure that the latest version of your Worker is fully deployed (wrangler versions deploy) before modifying secrets. Alternatively, you can use the Cloudflare dashboard to modify secrets and deploy the version. - - Note: This limitation will be addressed in an upcoming release.] + [Error: Secret edit failed. You attempted to modify a secret, but the latest version of your Worker isn't currently deployed. + This limitation exists to prevent accidental deployment when using Worker versions and secrets together. + To resolve this, you have two options: + (1) use the \`wrangler versions secret put\` instead, which allows you to update secrets without deploying; or + (2) deploy the latest version first, then modify secrets. + Alternatively, you can use the Cloudflare dashboard to modify secrets and deploy the version.] `); }); }); diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index f810e2db6b3e..966920005840 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -216,11 +216,12 @@ export const secretPutCommand = createCommand({ } catch (e) { if (e instanceof APIError && e.code === VERSION_NOT_DEPLOYED_ERR_CODE) { throw new UserError( - "Secret edit failed. You attempted to modify a secret, but the latest version of your Worker isn't currently deployed. " + - "Please ensure that the latest version of your Worker is fully deployed " + - "(wrangler versions deploy) before modifying secrets. " + - "Alternatively, you can use the Cloudflare dashboard to modify secrets and deploy the version." + - "\n\nNote: This limitation will be addressed in an upcoming release." + "Secret edit failed. You attempted to modify a secret, but the latest version of your Worker isn't currently deployed.\n" + + "This limitation exists to prevent accidental deployment when using Worker versions and secrets together.\n" + + "To resolve this, you have two options:\n" + + "(1) use the `wrangler versions secret put` instead, which allows you to update secrets without deploying; or\n" + + "(2) deploy the latest version first, then modify secrets.\n" + + "Alternatively, you can use the Cloudflare dashboard to modify secrets and deploy the version." ); } else { throw e; From 4714ca12c1f24c7e3553d3bfd2812a833a07826c Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 14 Jan 2026 00:40:00 +0000 Subject: [PATCH 2/2] Add `MF-Original-Hostname` header when using the `upstream` option (#11883) --- .changeset/original-hostname-header.md | 18 ++++++++ packages/miniflare/README.md | 18 ++++++++ .../miniflare/src/workers/core/constants.ts | 7 +++ .../src/workers/core/entry.worker.ts | 8 ++++ packages/miniflare/test/index.spec.ts | 44 +++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 .changeset/original-hostname-header.md diff --git a/.changeset/original-hostname-header.md b/.changeset/original-hostname-header.md new file mode 100644 index 000000000000..7e3a6c65400c --- /dev/null +++ b/.changeset/original-hostname-header.md @@ -0,0 +1,18 @@ +--- +"miniflare": minor +--- + +Add `MF-Original-Hostname` header when using the `upstream` option + +When using the `upstream` option in Miniflare, the `Host` header is rewritten to match the upstream server, which means the original hostname is lost. This change adds a new `MF-Original-Hostname` header that preserves the original hostname from the incoming request. + +This allows Workers to access the original hostname when proxying requests through an upstream server: + +```js +export default { + async fetch(request) { + const originalHostname = request.headers.get("MF-Original-Hostname"); + // originalHostname contains the hostname before it was rewritten + }, +}; +``` diff --git a/packages/miniflare/README.md b/packages/miniflare/README.md index 1c1901ece49e..7da2d6c0b530 100644 --- a/packages/miniflare/README.md +++ b/packages/miniflare/README.md @@ -742,6 +742,24 @@ Options shared between all Workers/"nanoservices". useful when testing Workers that act as a proxy, and not as origins themselves. + When `upstream` is set, the `Host` header is rewritten to match the upstream + server. To preserve the original hostname, Miniflare adds an + `MF-Original-Hostname` header containing the original `Host` value: + + ```js + export default { + async fetch(request) { + // When upstream is set, Host header contains the upstream hostname + const upstreamHost = request.headers.get("Host"); + // Original hostname is preserved in MF-Original-Hostname + const originalHost = request.headers.get("MF-Original-Hostname"); + return new Response( + `Original: ${originalHost}, Upstream: ${upstreamHost}` + ); + }, + }; + ``` + - `cf?: boolean | string | Record` Controls the object returned from incoming `Request`'s `cf` property. diff --git a/packages/miniflare/src/workers/core/constants.ts b/packages/miniflare/src/workers/core/constants.ts index 2800a3cedf46..e73f1c445205 100644 --- a/packages/miniflare/src/workers/core/constants.ts +++ b/packages/miniflare/src/workers/core/constants.ts @@ -2,6 +2,13 @@ export const CoreHeaders = { CUSTOM_FETCH_SERVICE: "MF-Custom-Fetch-Service", CUSTOM_NODE_SERVICE: "MF-Custom-Node-Service", ORIGINAL_URL: "MF-Original-URL", + /** + * Stores the original hostname when using the `upstream` option. + * When requests are proxied to an upstream, the `Host` header is rewritten + * to match the upstream. This header preserves the original hostname + * so Workers can access it if needed. + */ + ORIGINAL_HOSTNAME: "MF-Original-Hostname", PROXY_SHARED_SECRET: "MF-Proxy-Shared-Secret", DISABLE_PRETTY_ERROR: "MF-Disable-Pretty-Error", ERROR_STACK: "MF-Experimental-Error-Stack", diff --git a/packages/miniflare/src/workers/core/entry.worker.ts b/packages/miniflare/src/workers/core/entry.worker.ts index deefc4e9d86b..d606cbf61cd2 100644 --- a/packages/miniflare/src/workers/core/entry.worker.ts +++ b/packages/miniflare/src/workers/core/entry.worker.ts @@ -76,6 +76,8 @@ function getUserRequest( // If Miniflare was configured with `upstream`, then we use this to override the url and host in the request. const upstreamUrl = env[CoreBindings.TEXT_UPSTREAM_URL]; + // Store the original hostname before it gets rewritten by upstream + const originalHostname = upstreamUrl !== undefined ? url.host : undefined; if (upstreamUrl !== undefined) { // If a custom `upstream` was specified, make sure the URL starts with it let path = url.pathname + url.search; @@ -109,6 +111,12 @@ function getUserRequest( request.headers.set("Host", url.host); } + // Set the original hostname header when using upstream, so Workers can + // access the original hostname even after the Host header is rewritten + if (originalHostname !== undefined) { + request.headers.set(CoreHeaders.ORIGINAL_HOSTNAME, originalHostname); + } + if (clientIp && !request.headers.get("CF-Connecting-IP")) { const ipv4Regex = /(?.*?):\d+/; const ipv6Regex = /\[(?.*?)\]:\d+/; diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index cf4130ff3d4f..34f73f5bc01d 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -1255,6 +1255,50 @@ test("Miniflare: custom upstream as origin", async () => { host: upstream.http.host, }); }); +test("Miniflare: custom upstream sets MF-Original-Hostname header", async () => { + const upstream = await useServer((req, res) => { + res.end(`upstream`); + }); + const mf = new Miniflare({ + upstream: upstream.http.toString(), + modules: true, + script: `export default { + async fetch(request) { + return Response.json({ + host: request.headers.get("Host"), + originalHostname: request.headers.get("MF-Original-Hostname") + }); + } + }`, + }); + useDispose(mf); + // Check that original hostname is preserved when using upstream + const res = await mf.dispatchFetch( + "https://my-original-host.example.com:8080/path?a=1" + ); + expect(await res.json()).toEqual({ + host: upstream.http.host, + originalHostname: "my-original-host.example.com:8080", + }); +}); +test("Miniflare: MF-Original-Hostname header not set without upstream", async () => { + const mf = new Miniflare({ + modules: true, + script: `export default { + async fetch(request) { + return Response.json({ + originalHostname: request.headers.get("MF-Original-Hostname") + }); + } + }`, + }); + useDispose(mf); + // Check that original hostname header is not set when not using upstream + const res = await mf.dispatchFetch("https://random:0/path?a=1"); + expect(await res.json()).toEqual({ + originalHostname: null, + }); +}); test("Miniflare: set origin to original URL if proxy shared secret matches", async () => { const mf = new Miniflare({ unsafeProxySharedSecret: "SOME_PROXY_SHARED_SECRET_VALUE",