Skip to content

getValueOf returns a stale cached value after the response file is rewritten in the same process (MCP server) #251

Description

@xaaha

Summary

getValueOf memoizes results in a package-level cache that lives for the entire process and is never invalidated. In the long-lived MCP server, once a file:key pair is read it is frozen for the life of the process — even if hulak itself rewrites that file. This breaks the common auth pattern where one request saves a token response and a later request reads it with getValueOf.

Reproduction

A request reads a token from a response file another request writes:

# getUserPlan.hk.yaml
headers:
  Authorization: Bearer {{getValueOf "access_token" "auth/getAuth.hk_response.json"}}

In a single MCP-server session:

  1. call_request getUserPlan → the token file holds an old/expired token → request fails 401. (This first read caches the old token.)
  2. call_request getAuth --save → HTTP 200, writes a fresh token to auth/getAuth.hk_response.json. Confirmed on disk (cat shows the fresh token).
  3. call_request getUserPlan again → still 401. Debug output shows the Authorization header carrying the old token, not the fresh one on disk.
  4. Repeating never helps. Hardcoding the token makes the call succeed, confirming everything else is fine.

Root cause

A process-lifetime cache in pkg/actions/actions.go:

  • pkg/actions/actions.go:24-30 — package-global valuesCache map[string]valueCache, lives for the whole process.
  • GetValueOf (pkg/actions/actions.go:33-64) keys the cache as "<fileName>:<key>". On a cache hit it returns the stored value and never reads the file; only on a miss does it call processValueOfreadJSONFile (actions.go:211-244, a real os.ReadFile), then stores the result permanently. There is no invalidation anywhere — no TTL, no mtime check, no delete.
  • The MCP server is a single long-lived process (pkg/mcp/server.go:193), so valuesCache persists across every call_request.
  • The save path (pkg/apiCalls/writeResponse.go:147, os.WriteFile) writes the file but never touches valuesCache, so the cache goes stale relative to disk.

getValueOf is wired as a template func evaluated on every request (pkg/envparser/replaceVars.go:94), so step 1 seeds the cache and steps 3+ hit it.

Ruled out (not the cause)

  • Path/instance mismatch — read (resolveJSONFilePathfilepath.Abs, actions.go:156-171) and write (writeResponse.go:132-134) both resolve against the same project dir to the same file; the on-disk cat confirms the write landed where the read resolves.
  • Write/flush race — the save is a synchronous, fully-flushed os.WriteFile that completes before the next call.

Proposed fix

Drop the process-lifetime memoization for getValueOf (or gate readJSONFile behind an mtime check). A cache keyed only on file:key with no mtime/generation component is fundamentally unsafe for files hulak itself rewrites mid-process — which is exactly the getAuthgetValueOf pattern. The memoization buys little for response files that change between calls and is the direct cause of the staleness.

(A more minimal alternative — evict matching cache entries on every response-file write via an exported actions.InvalidateFile(path) — would also work, but dropping/mtime-gating the cache is the cleaner, correct-by-construction fix.)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions