Skip to content

fix(core): avoid mutating runtimeEnv when emptyStringAsUndefined is true#411

Open
FrancoKaddour wants to merge 1 commit into
t3-oss:mainfrom
FrancoKaddour:fix/empty-string-as-undefined-mutates-env
Open

fix(core): avoid mutating runtimeEnv when emptyStringAsUndefined is true#411
FrancoKaddour wants to merge 1 commit into
t3-oss:mainfrom
FrancoKaddour:fix/empty-string-as-undefined-mutates-env

Conversation

@FrancoKaddour

Copy link
Copy Markdown

When emptyStringAsUndefined: true is set, the current implementation iterates over runtimeEnv and calls delete runtimeEnv[key] for every empty-string entry. If the caller passes process.env (the most common pattern), this permanently removes those keys from the global process.env object for the lifetime of the process.

Reproduction

process.env.DATABASE_URL = "";

createEnv({
  server: { DATABASE_URL: z.string().optional() },
  runtimeEnv: process.env,
  emptyStringAsUndefined: true,
});

console.log("DATABASE_URL" in process.env); // false — key was deleted

Any code that reads process.env.DATABASE_URL after createEnv (e.g. Prisma, a logger, another createEnv call) sees undefined instead of "". The effect is silent and hard to debug.

Fix

Build a filtered copy of the env object instead of mutating it in place:

// before
const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
if (emptyStringAsUndefined) {
  for (const [key, value] of Object.entries(runtimeEnv)) {
    if (value === "") {
      delete runtimeEnv[key];
    }
  }
}

// after
const rawRuntimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
const runtimeEnv = emptyStringAsUndefined
  ? (Object.fromEntries(
      Object.entries(rawRuntimeEnv).filter(([, v]) => v !== ""),
    ) as typeof rawRuntimeEnv)
  : rawRuntimeEnv;

The original runtimeEnv / process.env reference is never touched.

Tests added (in packages/core/test/smoke-zod4.test.ts):

  • Verifies the passed object is not mutated after createEnv
  • Verifies empty strings are still treated as undefined in validation (required field throws)
  • Verifies schema defaults are applied when the env var is an empty string

`emptyStringAsUndefined` was deleting keys directly from the `runtimeEnv`
object. When callers pass `process.env` (the common case), this permanently
removes those keys from the global environment, breaking any code that reads
them afterward.

Fix by building a filtered copy instead of mutating in place.
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

@FrancoKaddour is attempting to deploy a commit to the t3-oss Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant