Skip to content

fix(variables): validate variable keys before writing them - #3155

Open
levivannoort wants to merge 3 commits into
mainfrom
fix/variable-key-validation
Open

fix(variables): validate variable keys before writing them#3155
levivannoort wants to merge 3 commits into
mainfrom
fix/variable-key-validation

Conversation

@levivannoort

Copy link
Copy Markdown
Member

What

Adds variable key validation to the console, in every flow that writes one, and fixes three flows that handled a rejected key badly.

Why

appwrite/appwrite#13181 makes the API reject variable keys that are not valid environment variable names (^[A-Za-z_]\w*$, max 255). The console had no key validation anywhere — only a value length check — so keys with hyphens, dots, spaces or a leading digit reached the API and returned a bare server error.

Three flows turn that error into real damage:

  1. Orphaned resources. create-function/* and create-site/* create the function or site (and its proxy rule) before the variables. A rejected key aborts mid-flow and leaves a resource with no deployment.
  2. Legacy keys became uneditable. The update modal resends the stored key on a value-only edit, so a key stored before the rule existed started failing on every save — the user could not even correct its value.
  3. Promote-to-global could destroy data. The conflicting branch deletes the existing global variable before creating its replacement; if the create is rejected, the original is gone.

Changes

New src/lib/helpers/variables.tsisValidVariableKey, getVariableKeyError, getVariableValueError, validateVariables (+ unit tests). The existing normalizeDetectedVariables / mergeVariables are unchanged.

Validation wired into all seven entry points: both create modals, both update modals, the .env import modal, and both raw editors. The existing 8192-character value check is folded into the shared helper, so each call site keeps one check instead of two.

Behaviour fixes:

  • updateVariablesModal validates the key only when it changed and omits it otherwise; handleVariableSecret never sends it. The endpoint treats key as sparse (Update.php:107) and the SDK drops undefined, so a value-only edit on a pre-existing MY-KEY now succeeds. sdkUpdateVariable's key parameter widens to string | undefined in three components.
  • All 8 create-function/create-site flows validate before the create() call, so an invalid key can no longer orphan a resource.
  • Promote-to-global validates before the delete.
  • Keys that fail the rule are flagged with a warning icon and tooltip in both variables tables — they are ignored at build and runtime today, silently.
  • The batch writes use allSettled and name the keys that failed, instead of surfacing one rejection for the whole batch.

Testing

bun run check (0 errors, 87 warnings — unchanged baseline), bun run lint (0 errors), bun run test:unit (243 passed), bun run build all pass.

src/lib/helpers/oauth2-cimd.test.ts fails to load locally on a missing APPWRITE_ENDPOINT; it arrives from 9dde997 and is untouched here.

The API now rejects variable keys that are not valid environment variable
names, but the console had no key validation anywhere — only a value
length check. Keys with hyphens, dots, spaces or a leading digit reached
the API and came back as a bare server error, and three flows handled
that badly:

- create-function and create-site create the resource (and its proxy
  rule) before the variables, so a rejected key left a function or site
  with no deployment behind
- the update modal resends the stored key on a value-only edit, so a key
  stored before the rule existed could no longer have its value changed
- promote-to-global deletes the existing global variable before creating
  its replacement, so a rejected key destroyed the original

Add a shared validator and check keys before submitting, in every entry
point that writes them. Send the key on update only when it changed, so
the API keeps the stored one and existing keys stay editable. Flag keys
that cannot be used as environment variable names in the variables
tables, since those are ignored at build and runtime. Report per-key
failures from the batch writes instead of a single rejection.

Server-side rule: appwrite/appwrite#13181

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@appwrite

appwrite Bot commented Aug 12, 2026

Copy link
Copy Markdown

Console (appwrite/console)

Project ID: 688b7bf400350cbd60e9

Sites (1)
Site Status Logs Preview QR
 console-stage
688b7cf6003b1842c9dc
Ready Ready View Logs Preview URL QR Code

Tip

Every Git commit and branch gets its own deployment URL automatically

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds shared environment-variable key and value validation across creation, editing, and import flows, while improving handling of legacy keys and partial batch failures.

  • Validates keys before creating functions or sites, preventing rejected variables from leaving orphaned resources.
  • Omits unchanged keys from updates so legacy variables remain editable.
  • Filters discarded .env entries before validation and reports failed batch writes by key.
  • Warns users when stored keys cannot be used at build or runtime.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/lib/helpers/variables.ts Introduces centralized key and value validation matching the documented identifier and length constraints.
src/lib/components/variables/importVariablesModal.svelte Correctly filters entries that will not be written before validation, resolving the previously reported import failure.
src/routes/(console)/project-[region]-[project]/updateVariablesModal.svelte Validates renamed keys while omitting unchanged keys from sparse updates, preserving edits to legacy variables.
src/routes/(console)/project-[region]-[project]/rawVariableEditor.svelte Adds shared validation, sparse key updates, and per-key reporting for rejected bulk operations.
src/routes/(console)/project-[region]-[project]/updateVariables.svelte Adds validation before destructive promotion and improves batch-create and legacy-key update handling.
src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte Validates variables before function creation to avoid partially configured resources.
src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte Validates variables before site creation to avoid partially configured resources.

Fix All in Greploop

Reviews (6): Last reviewed commit: "fix(variables): filter valueless entries..." | Re-trigger Greptile

Comment thread src/lib/components/variables/updateVariableModal.svelte
);
}
});
const validationError = validateVariables(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn’t this also validate keys that are already stored? 🤔 If a project contains a legacy key (examplews MY-KEY), updating only its value would fail here before sdkUpdateVariable(..., undefined, ...) is ever reached. Should we only validate keys for newly added variables and keep validating values for existing ones?
cc @greptile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, but the concern is scoped to the wrong component. In createVariableModal.svelte, validateVariables is only called on newVariables — the rows the user is currently entering in the form. The pre-existing variables prop is never passed through validation here, so legacy stored keys are safe.

The concern is real in updateVariableModal.svelte though. There, pair is initialized from selectedVar (the stored variable), and validateVariables([pair]) runs unconditionally on submit — it doesn't check whether pair.key actually changed. So if a user opens the update modal on a legacy MY-KEY variable to change only the value, the key validation fires and blocks them before sdkUpdateVariable (with undefined key) is ever reached. That's the breakage the PR description says it fixes, but looking at the actual code in updateVariableModal.svelte the guard isn't there.

The fix would be to skip key validation when the key is unchanged:

function handleVariable() {
    const keyChanged = pair.key !== selectedVar?.key;
    const toValidate = keyChanged ? pair : { key: undefined, value: pair.value };
    const validationError = validateVariables([toValidate]);
    ...
}

But validateVariablesgetVariableKeyError returns 'Variable key is required' for an undefined key, so validateVariables would also need to treat key === undefined as "skip key check" (i.e. only validate value). Worth addressing before merge — as-is, updateVariableModal.svelte still breaks value-only edits on any pre-existing invalid key.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked both create modals and both update modals — no change needed here, and the split you describe is what is already implemented.

Create modals (lib/components/variables/createVariableModal.svelte, project-[region]-[project]/createVariableModal.svelte): validateVariables only ever receives newVariables, the rows being typed into the form. The pre-existing variables prop is never passed through it, so stored keys are untouched.

Update modal (project-[region]-[project]/updateVariablesModal.svelte) — this is the one reachable from the post-create variables table, and it already guards on the key having changed:

const keyChanged = pair.key !== originalKey;
const validationError = keyChanged
    ? validateVariables([pair])
    : getVariableValueError(pair.key, pair.value);

so a value-only edit on a legacy MY-KEY validates the value only, and dispatches key: undefined so the API keeps the stored key.

lib/components/variables/updateVariableModal.svelte (the singular one, which greptile pointed at) validates unconditionally, but as you noted in the other thread it is only reachable through EnvironmentVariables in the two create-flow configuration components, where the key is always user-entered.

// the API keep the stored one.
await sdkUpdateVariable(variable.$id, undefined, newValue, false);
}
delete vars[variable.key];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this now uses Promise.allSettled, could a failed update leave the key in vars until the second pass? If so, wouldn’t that cause it to be processed again and end up calling sdkUpdateVariablewith the key, which is what this PR is trying to avoid? Would it make sense to move delete vars[variable.key] before the awaited update? 🤔
cc @greptile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the concern is valid. If sdkUpdateVariable or sdkDeleteVariable throws in the first pass, the delete vars[variable.key] line is never reached. The key stays in vars, and the second pass picks it up as an "existing variable" and calls:

await sdkUpdateVariable(existingVariable.$id, key, vars[key], false);

…this time with the key argument, which is precisely what the PR is trying to avoid for legacy invalid keys.

Moving the delete before the await alone is slightly risky if the value is needed later, but a try/finally is the clean fix:

Suggested change
delete vars[variable.key];
editableVariables.map(async (variable) => {
const newValue = vars[variable.key] ?? null;
try {
if (newValue === null) {
await sdkDeleteVariable(variable.$id);
} else if (newValue !== variable.value) {
// The key is unchanged here, so leave it out and let
// the API keep the stored one.
await sdkUpdateVariable(variable.$id, undefined, newValue, false);
}
} finally {
delete vars[variable.key];
}

finally guarantees the key is removed from vars whether the operation succeeds or fails, so the second pass never re-processes an existing key with the key argument. Failures are still captured by existingResults and surfaced in the error summary — nothing is silently swallowed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — you were right, and it was worse than just the key being resent: the failed variable got a second write attempt too. Fixed in 0feeb2a by claiming the key before the awaits rather than after:

const newValue = vars[variable.key] ?? null;

// Claim the key up front. A rejected write must not leave it
// behind for the second pass, which would retry it as a new
// variable and send the stored key along with it.
delete vars[variable.key];

I went with moving the delete up rather than try/finally since newValue is already captured in a local and nothing reads vars[variable.key] afterwards, so there is no state to preserve across the await. Failures are still collected by existingResults and named in the error summary.

One note on scope: the delete branch was already safe, since newValue === null means the key is absent from vars and the delete was a no-op. It was only the update branch that could leak.

@HarshMN2345

Copy link
Copy Markdown
Member

@greptile re-review

A rejected update left its key in vars, so the second pass picked it up
as a new variable and resent the stored key — the exact call the sparse
update is meant to avoid for keys that predate the identifier rule.
@levivannoort

Copy link
Copy Markdown
Member Author

@greptile re-review

Comment thread src/lib/components/variables/importVariablesModal.svelte
An entry with an empty value is discarded rather than written, so an
invalid key on one of them was rejecting the whole file and taking the
valid variables down with it.
@levivannoort

Copy link
Copy Markdown
Member Author

@greptile re-review

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.

2 participants