fix(secret-reference): keep literal reference when secret does not exist#7032
fix(secret-reference): keep literal reference when secret does not exist#7032claude[bot] wants to merge 2 commits into
Conversation
…s not exist A reference to a non-existent secret previously resolved to an empty string (regression from #6844), breaking services that validate env vars are non-empty. fetchSecret now reports an `exists` flag so the substitution in recursivelyExpandSecret can distinguish "secret not found" (leave the literal ${REF}) from "found with empty value" (resolve to ""). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CaforYBUXs5dkUfUBwSKS5
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e3a28c71f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (secretCache?.[cacheKey]) { | ||
| return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; | ||
| const cachedSecret = secretCache[cacheKey][secretKey]; | ||
| if (cachedSecret) return { ...cachedSecret, exists: true }; |
There was a problem hiding this comment.
Do not mark negative cache hits as existing
When a missing reference is fetched below, its { exists: false } result is written into secretCache; on the next lookup this branch sees that truthy cached object and overwrites it to exists: true. A value such as ${MISSING} ${MISSING} (or two secrets expanded by the same factory that both reference MISSING) will therefore remove the second/future occurrence(s) as an empty string instead of preserving the literal, which is exactly the case this fix is meant to protect.
Useful? React with 👍 / 👎.
|
| Filename | Overview |
|---|---|
| backend/src/services/secret-v2-bridge/secret-reference-fns.ts | Adds exists flag to fetchSecret and gates substitution on it — correct intent, but writing non-existent secret stubs into secretCache causes the second reference to the same missing key to incorrectly resolve to "". |
| backend/e2e-test/routes/v3/secret-reference.spec.ts | Adds a focused e2e test for the non-existent secret case; the test only covers a single reference per value and does not catch the duplicate-reference or cross-secret cache bug. |
Comments Outside Diff (1)
-
backend/src/services/secret-v2-bridge/secret-reference-fns.ts, line 197-199 (link)Cache poisoned for non-existent secrets
recursivelyExpandSecretwrites thefetchSecretresult (includingexists: false) intosecretCacheat lines 199 and 220. When a second reference to the same non-existent secret is later looked up — either in the same value ("${MISSING} and ${MISSING}") or in a different secret expanded by the same factory instance (which sharessecretCache) —fetchSecretfinds a truthy object ({ value: "", tags: [], exists: false }) for that key and returns{ ...cachedSecret, exists: true }, incorrectly reporting the secret as existing and substituting"". The fix: only cache the secret if it exists, e.g.if (referredValue.exists) { secretCache[cacheKey][secretKey] = referredValue; }. The same pattern applies at lines 219-220 for cross-environment references.
Reviews (1): Last reviewed commit: "fix(secret-reference): keep literal ${re..." | Re-trigger Greptile
Cache the exists flag inside the cached secret object so a repeated
reference to a missing secret (e.g. ${MISSING} ${MISSING}) keeps
exists:false on every lookup instead of the cache-hit path always
returning exists:true. Add e2e coverage for the repeated-missing case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CaforYBUXs5dkUfUBwSKS5
|
@claude, did you address this?
|
|
Yes — fixed in 538d279. The root cause was as you described: Rather than skip caching for missing secrets, I stored |
Requested by Daniel Hougaard · Slack thread
Before / After
Before: A secret reference like
${SOME_SECRET}pointing to a secret that does not exist resolved to an empty string (""). This was a regression introduced by #6844 ("check for empty value on reference"), which removed theif (referencedSecretValue)guard. It broke customer production services that validate their environment variables are non-empty.After:
${REF}text in the output, untouched."") still resolves to""— the behavior feat(secret-reference): check for empty value on reference #6844 intended is preserved.How
The two cases above were previously indistinguishable:
fetchSecretreturned{ value: "", tags: [] }for both "secret not found" and "found with empty value".fetchSecretnow returns anexists: booleanflag on every return path (folder missing, key absent, DB error =>exists: false; found in map / cache hit =>exists: true), distinguishing the two cases.recursivelyExpandSecretis now gated onexistsrather than on value truthiness. When the referenced secret does not exist, the literal${REF}is left in place.A focused e2e test was added asserting that
hello ${NON_EXISTENT_SECRET}resolves to the literalhello ${NON_EXISTENT_SECRET}, alongside the existinghello ${EMPTY}=>"hello "test which continues to pass.Note: the v1 legacy expansion path in
secret-fns.tsstill has the old unconditional substitution and could receive the same treatment as a follow-up.Relates to / partially reverts the conflation introduced by #6844.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CaforYBUXs5dkUfUBwSKS5
Generated by Claude Code