[wrangler] Fix case-insensitive-env leaking stale duplicate keys on override - #14999
[wrangler] Fix case-insensitive-env leaking stale duplicate keys on override#14999mittalpk wants to merge 2 commits into
Conversation
…verride The case-insensitive Proxy used for .env loading on Windows tracked the current casing for a key in a side Map, but its set/defineProperty/ deleteProperty traps never removed the old differently-cased property from the underlying target object. get/has still resolved correctly through the tracking map, but Object.keys/for...in/JSON.stringify/spread over the resulting object (which is assigned directly to process.env) exposed both the stale and current key. Fix set/defineProperty to delete the previous casing's property before writing the new one, and deleteProperty to remove the tracked casing rather than whatever casing the caller happened to pass.
🦋 Changeset detectedLatest commit: 0fc6313 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
|
|
||
| On Windows, `wrangler` loads `.env` values through a case-insensitive `Proxy` wrapper so lookups like `env.PATH` and `env.Path` resolve to the same value, and this object is assigned directly to `process.env`. When a key was set again under a different casing (e.g. a value in `.env.local` overriding one from `.env` with different casing), the previous casing was never removed from the underlying object. `env.PATH`/`env.Path` still returned the correct, latest value, but anything that enumerates `process.env` — `Object.keys`, `for...in`, `JSON.stringify`, object spread, or a spawned subprocess inheriting the environment — would see both the stale and current key. | ||
|
|
||
| The proxy's `set`, `defineProperty`, and `deleteProperty` traps now keep exactly one entry per case-insensitive key, so enumeration always reflects the current value. |
There was a problem hiding this comment.
🟡 Release note explains internal machinery instead of user impact
The release note text (.changeset/case-insensitive-env-stale-keys.md:7) describes internal implementation machinery rather than the user-facing effect, so the published changelog reads like maintainer notes.
Impact: Users reading the changelog get internal details they cannot act on instead of a clear description of what changed for them.
Repository changeset rule about implementation details
REVIEW.md states: "Changesets should target users of the tools (e.g. Wrangler users) rather than maintainers. Avoid including implementation details ... Instead, focus on user-facing impact and benefits." The middle paragraph describes the case-insensitive Proxy wrapper, the assignment to process.env, and "the previous casing was never removed from the underlying object" — all internal mechanics. The last paragraph (line 9) already conveys the user-facing impact and could stand alone.
Was this helpful? React with 👍 or 👎 to provide feedback.
Per review feedback: changesets should describe what changed for users, not implementation details like which Proxy traps were touched.
Fix
case-insensitive-env.tsleaking stale, differently-cased duplicate keys.On Windows,
wranglerloads.envvalues through a case-insensitiveProxywrapper (packages/wrangler/src/config/case-insensitive-env.ts) so lookups likeenv.PATHandenv.Pathresolve to the same value, and this object is assigned directly toprocess.env(packages/wrangler/src/config/dot-env.ts). When a key is set again under a different casing (e.g..env.localoverriding a value from.envwith different casing), the previous casing's property was never removed from the underlying target object.get/hasstill resolved correctly through the internal tracking map, but anything that enumeratesprocess.env—Object.keys,for...in,JSON.stringify, object spread, or a spawned subprocess inheriting the environment — saw both the stale and current key.The
set,defineProperty, anddeletePropertytraps now keep exactly one own-property per case-insensitive key, so enumeration always reflects only the current value. (deletePropertyhad the same class of bug in the other direction: it deleted from the internal tracking map by canonical key but issuedReflect.deletePropertyusing whatever casing the caller passed, sodelete env.pathafterenv.PATH = ...left an orphaned, untrackedPATHproperty on the target — fixed the same way, by resolving the tracked casing first.)Repro (before the fix):
Note
This is a contribution from an AI agent: Claude Code (Anthropic), running under direct human supervision and review.