Skip to content
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
18 changes: 18 additions & 0 deletions .changeset/original-hostname-header.md
Original file line number Diff line number Diff line change
@@ -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
},
};
```
5 changes: 5 additions & 0 deletions .changeset/ten-pants-wash.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions packages/miniflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>`

Controls the object returned from incoming `Request`'s `cf` property.
Expand Down
7 changes: 7 additions & 0 deletions packages/miniflare/src/workers/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions packages/miniflare/src/workers/core/entry.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = /(?<ip>.*?):\d+/;
const ipv6Regex = /\[(?<ip>.*?)\]:\d+/;
Expand Down
44 changes: 44 additions & 0 deletions packages/miniflare/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 6 additions & 3 deletions packages/wrangler/src/__tests__/secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.]
`);
});
});
Expand Down
11 changes: 6 additions & 5 deletions packages/wrangler/src/secret/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading