Skip to content

fix(cli): handle non-JSON upstream in fetchRemotePrompt#1282

Open
calebcgates wants to merge 1 commit into
VoltAgent:mainfrom
calebcgates:fix/cli-prompts-fetch-non-json-response
Open

fix(cli): handle non-JSON upstream in fetchRemotePrompt#1282
calebcgates wants to merge 1 commit into
VoltAgent:mainfrom
calebcgates:fix/cli-prompts-fetch-non-json-response

Conversation

@calebcgates
Copy link
Copy Markdown

@calebcgates calebcgates commented May 13, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

fetchRemotePrompt in packages/cli/src/commands/prompts.ts (line 360) carefully handles 404 (returns null) and non-2xx (throws a rich error: Failed to fetch prompt 'name': status statusText - body). But the final return (await response.json()) as RemotePrompt; on line 398 is unguarded.

When the VoltOps prompts API (or any proxy in front of it — corporate CDN, captive portal, mid-deploy HTML rollback) returns a 200 OK with an HTML body, response.json() throws a raw SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON. That propagates through the commander .action() handler which prints error.message to stderr — so the user sees the raw SyntaxError text with no hint of which prompt was being fetched or that the upstream was misbehaving:

Prompt pull failed
Unexpected token '<', "<!DOCTYPE "... is not valid JSON

This affects volt prompts pull and volt prompts push (both code paths call fetchRemotePrompt).

What is the new behavior?

fetchRemotePrompt now wraps await response.json() in a try/catch and rethrows a rich error mirroring the existing non-2xx handler shape on lines 389-396:

Prompt pull failed
Failed to parse prompt 'foo' response: Unexpected token '<', "<!DOCTYPE "... is not valid JSON. The upstream returned a non-JSON body (status 200 OK).

The user immediately understands the upstream returned something unexpected and can investigate proxy / CDN / VoltOps deployment status, instead of suspecting their own CLI installation.

Added two vitest specs covering the non-JSON failure path (including a "preserves the underlying parser reason" case that verifies prompt name + HTTP status correlation) and one happy-path regression spec.

Notes for reviewers

  • Source change is +12 / -1 in one file (packages/cli/src/commands/prompts.ts). Spec is a new file (packages/cli/src/commands/prompts.spec.ts, 86 lines, 3 cases). Changeset is a new .changeset/cli-prompts-fetch-non-json-response.md (patch on @voltagent/cli).
  • The error message shape mirrors the existing line 389-396 handler exactly. No new strings to localize, no new error class.
  • fetchRemotePrompt is currently module-private; the spec mirrors the parse contract in a local helper rather than importing it. Happy to follow up with export const fetchRemotePrompt = ... and a direct import if you'd prefer — that's a one-character source change and unblocks future tests on the same function.
  • Tested locally: pnpm --filter @voltagent/cli test (3 passing), pnpm --filter @voltagent/cli lint (clean — biome), pnpm --filter @voltagent/cli build (clean).
  • No related issue — happy to open one and link if you'd prefer issue-first.

Summary by cubic

Handle non-JSON 200 OK responses in the CLI prompt fetch path by catching JSON parse errors and throwing a clear message with the prompt name and HTTP status. This prevents raw SyntaxError output in volt prompts pull/volt prompts push when the API or a proxy returns HTML.

  • Bug Fixes
    • Wrap response.json() in a try/catch in fetchRemotePrompt and emit: "Failed to parse prompt '' response: . The upstream returned a non-JSON body (status )."
    • Add tests for the non-JSON failure and happy path; publish a patch changeset for @voltagent/cli.

Written for commit 02987546c25b3988637ec96a129f99a4ef061078. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced error handling in volt prompts pull and volt prompts push to provide clearer error messages when the upstream API returns non-JSON responses, including prompt name and HTTP status details.

Review Change Stack

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 13, 2026

🦋 Changeset detected

Latest commit: 0298754

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/cli Patch

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

This PR adds JSON parsing error handling to the fetchRemotePrompt function. When the VoltOps API (or a proxy) returns non-JSON responses, the code now throws a structured error message that includes the prompt name and HTTP status details, instead of exposing a raw SyntaxError.

Changes

JSON parsing error guard for remote prompts

Layer / File(s) Summary
JSON parsing error guard implementation and validation
packages/cli/src/commands/prompts.ts, packages/cli/src/commands/prompts.spec.ts, .changeset/cli-prompts-fetch-non-json-response.md
fetchRemotePrompt wraps response.json() in try/catch to catch JSON parse failures and throw an error message that includes the prompt name and HTTP status/statusText. Test suite validates the guard with a local helper that mirrors the production contract: rich error on 200+non-JSON, preserved parse reason, and successful JSON parse on valid input. Changeset documents the error handling improvement for the patch release.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A prompt once came back as HTML so bright,
Where JSON was promised—oh what a plight!
But now we catch errors with grace and with care,
With status and name, our fix laid out fair.
Non-JSON no longer makes cryptic our code!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(cli): handle non-JSON upstream in fetchRemotePrompt' clearly and specifically summarizes the main change: adding error handling for non-JSON upstream responses in the prompt fetch function.
Description check ✅ Passed The description is comprehensive and complete, covering all required sections: checklist items are marked, current behavior and new behavior are detailed with code examples, and reviewer notes are provided.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/prompts.spec.ts`:
- Line 77: Replace the direct use of JSON.stringify in the test response
construction with the safeStringify utility: import safeStringify from
'@voltagent/internal' (or add it to existing imports in prompts.spec.ts) and
call safeStringify(payload) where Response(JSON.stringify(payload), ...) is
used; ensure the import name matches how safeStringify is exported and update
the Response instantiation to use the safeStringify output so the test adheres
to the TS guideline banning JSON.stringify.
- Around line 15-27: The test uses JSON.stringify where our coding guideline
mandates safeStringify from `@voltagent/internal`: replace the
JSON.stringify(payload) call used in the test helper with
safeStringify(payload). Locate the reproduceParseContract helper and any other
places in this file where JSON.stringify is used for test payload serialization
(specifically the call that builds the mock response body) and import/use
safeStringify instead to ensure safe serialization consistent with project
utilities.

In `@packages/cli/src/commands/prompts.ts`:
- Line 409: The unchecked cast "parsed as RemotePrompt" must be replaced with
runtime validation: implement a type guard function (e.g., isRemotePrompt(obj):
obj is RemotePrompt) that checks required fields/types of the RemotePrompt
interface and use it before returning; in the function that currently does
"return parsed as RemotePrompt" call isRemotePrompt(parsed) and throw/handle an
error if it returns false so only validated RemotePrompt objects are returned.
Ensure the type guard inspects all unique properties used elsewhere (names/types
referenced by RemotePrompt) and update callers to rely on the guard rather than
the direct cast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bb69e76-5184-4216-a64e-d760f6d83173

📥 Commits

Reviewing files that changed from the base of the PR and between 08414ed and 0298754.

📒 Files selected for processing (3)
  • .changeset/cli-prompts-fetch-non-json-response.md
  • packages/cli/src/commands/prompts.spec.ts
  • packages/cli/src/commands/prompts.ts

Comment on lines +15 to +27
const reproduceParseContract = async (response: Response, name: string): Promise<unknown> => {
let parsed: unknown;
try {
parsed = await response.json();
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to parse prompt '${name}' response: ${reason}. ` +
`The upstream returned a non-JSON body (status ${response.status} ${response.statusText}).`,
);
}
return parsed;
};
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's confirm the file exists and get a sense of its structure
fd -type f "prompts.spec.ts"

Repository: VoltAgent/voltagent

Length of output: 234


🏁 Script executed:

# Also check for the production file that might contain fetchRemotePrompt
fd -type f "prompts.ts" | grep -v spec

Repository: VoltAgent/voltagent

Length of output: 234


🏁 Script executed:

# Check the overall structure of the packages/cli directory
ls -la packages/cli/src/commands/ 2>/dev/null || echo "Directory not found"

Repository: VoltAgent/voltagent

Length of output: 894


🏁 Script executed:

# Read the prompts.spec.ts file to understand the context
cat -n packages/cli/src/commands/prompts.spec.ts

Repository: VoltAgent/voltagent

Length of output: 3791


🏁 Script executed:

# Read the prompts.ts file to find fetchRemotePrompt and understand the production code
cat -n packages/cli/src/commands/prompts.ts | head -100

Repository: VoltAgent/voltagent

Length of output: 3774


🏁 Script executed:

# Search for fetchRemotePrompt definition in prompts.ts
rg "fetchRemotePrompt" packages/cli/src/commands/prompts.ts -A 10

Repository: VoltAgent/voltagent

Length of output: 938


🏁 Script executed:

# Check for safeStringify imports in the repository
rg "safeStringify" packages/cli/src/ --type ts

Repository: VoltAgent/voltagent

Length of output: 1536


🏁 Script executed:

# Get more context on the fetchRemotePrompt function to understand the full parse logic
rg "const fetchRemotePrompt" packages/cli/src/commands/prompts.ts -A 30

Repository: VoltAgent/voltagent

Length of output: 905


🏁 Script executed:

# Get more of fetchRemotePrompt including the JSON parse step
rg "const fetchRemotePrompt" packages/cli/src/commands/prompts.ts -A 50 | head -80

Repository: VoltAgent/voltagent

Length of output: 1469


🏁 Script executed:

# Check if there are any other JSON.stringify calls in the spec file
rg "JSON\.stringify" packages/cli/src/commands/prompts.spec.ts -n

Repository: VoltAgent/voltagent

Length of output: 127


Replace JSON.stringify with safeStringify at line 77.

The test helper uses JSON.stringify(payload) which violates the coding guideline requiring safeStringify from @voltagent/internal. Update line 77 to use safeStringify(payload) instead.

Regarding the test design: the reproduceParseContract helper is intentionally designed as a contract test (documented in the file comments). While direct testing of the production function would provide stronger isolation, the message-string assertions at lines 49–72 are designed to catch drift in the error format between test and production code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/prompts.spec.ts` around lines 15 - 27, The test
uses JSON.stringify where our coding guideline mandates safeStringify from
`@voltagent/internal`: replace the JSON.stringify(payload) call used in the test
helper with safeStringify(payload). Locate the reproduceParseContract helper and
any other places in this file where JSON.stringify is used for test payload
serialization (specifically the call that builds the mock response body) and
import/use safeStringify instead to ensure safe serialization consistent with
project utilities.


it("returns parsed JSON when upstream returns valid JSON", async () => {
const payload = { id: "p_1", type: "text", content: "hi" };
const response = new Response(JSON.stringify(payload), {
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read around line 77
if [ -f "packages/cli/src/commands/prompts.spec.ts" ]; then
  echo "=== File content around line 77 ==="
  cat -n "packages/cli/src/commands/prompts.spec.ts" | sed -n '70,85p'
else
  echo "File not found. Let's search for the file:"
  find . -name "prompts.spec.ts" -type f
fi

Repository: VoltAgent/voltagent

Length of output: 850


🏁 Script executed:

# Search for JSON.stringify usage in the target file
echo "=== All JSON.stringify usages in the file ==="
rg "JSON\.stringify" "packages/cli/src/commands/prompts.spec.ts" -n

Repository: VoltAgent/voltagent

Length of output: 173


🏁 Script executed:

# Check current imports in the file
echo "=== Import statements in the file ==="
head -30 "packages/cli/src/commands/prompts.spec.ts" | cat -n

Repository: VoltAgent/voltagent

Length of output: 1637


🏁 Script executed:

# Verify the correct import path for safeStringify
echo "=== Search for safeStringify in codebase ==="
rg "safeStringify" --type ts -l | head -20

Repository: VoltAgent/voltagent

Length of output: 1024


🏁 Script executed:

# Check import path for safeStringify in an actual usage
echo "=== Checking safeStringify import in one of the files ==="
head -20 "packages/cli/src/services/eval/dataset-push.ts" | rg "import.*safeStringify|from.*safeStringify" -A 1 -B 1

Repository: VoltAgent/voltagent

Length of output: 228


🏁 Script executed:

# Get more context around the import
rg "import.*safeStringify" "packages/cli/src/services/eval/dataset-push.ts" -A 1 -B 1

Repository: VoltAgent/voltagent

Length of output: 170


🏁 Script executed:

# Search across the codebase for safeStringify import pattern
echo "=== All safeStringify import patterns ==="
rg "import.*safeStringify.*from" --type ts | head -10

Repository: VoltAgent/voltagent

Length of output: 1211


Replace JSON.stringify with safeStringify.

Line 77 violates the TS guideline banning JSON.stringify.

Suggested fix
+import { safeStringify } from "@voltagent/internal";
...
-    const response = new Response(JSON.stringify(payload), {
+    const response = new Response(safeStringify(payload), {

As per coding guidelines, "Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = new Response(JSON.stringify(payload), {
import { safeStringify } from "@voltagent/internal";
...
const response = new Response(safeStringify(payload), {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/prompts.spec.ts` at line 77, Replace the direct use
of JSON.stringify in the test response construction with the safeStringify
utility: import safeStringify from '@voltagent/internal' (or add it to existing
imports in prompts.spec.ts) and call safeStringify(payload) where
Response(JSON.stringify(payload), ...) is used; ensure the import name matches
how safeStringify is exported and update the Response instantiation to use the
safeStringify output so the test adheres to the TS guideline banning
JSON.stringify.

);
}

return parsed as RemotePrompt;
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n packages/cli/src/commands/prompts.ts | sed -n '400,420p'

Repository: VoltAgent/voltagent

Length of output: 860


🏁 Script executed:

# Check for RemotePrompt type definition
rg "type RemotePrompt|interface RemotePrompt" packages/cli/src/commands/prompts.ts -A 5

Repository: VoltAgent/voltagent

Length of output: 202


🏁 Script executed:

# Get broader context around line 409 to understand the function
cat -n packages/cli/src/commands/prompts.ts | sed -n '380,425p'

Repository: VoltAgent/voltagent

Length of output: 1569


🏁 Script executed:

rg "JSON\.stringify" packages/cli/src/commands/prompts.ts

Repository: VoltAgent/voltagent

Length of output: 45


🏁 Script executed:

# Also check if safeStringify is used or imported
rg "safeStringify" packages/cli/src/commands/prompts.ts

Repository: VoltAgent/voltagent

Length of output: 762


Add runtime type validation before returning upstream payload.

The unchecked cast parsed as RemotePrompt on line 409 bypasses type safety for untrusted network data. Although the JSON parsing is wrapped in a try/catch, the response structure is never validated. Add a type guard to ensure the payload matches the RemotePrompt schema before casting.

Suggested fix
+const isRemotePrompt = (value: unknown): value is RemotePrompt => {
+  if (!value || typeof value !== "object") return false;
+  const v = value as Record<string, unknown>;
+  return (
+    typeof v.name === "string" &&
+    (v.type === "text" || v.type === "chat") &&
+    "prompt" in v
+  );
+};
...
-  return parsed as RemotePrompt;
+  if (!isRemotePrompt(parsed)) {
+    throw new Error(
+      `Failed to parse prompt '${name}' response: invalid payload shape (status ${response.status} ${response.statusText}).`,
+    );
+  }
+  return parsed;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return parsed as RemotePrompt;
const isRemotePrompt = (value: unknown): value is RemotePrompt => {
if (!value || typeof value !== "object") return false;
const v = value as Record<string, unknown>;
return (
typeof v.name === "string" &&
(v.type === "text" || v.type === "chat") &&
"prompt" in v
);
};
if (!isRemotePrompt(parsed)) {
throw new Error(
`Failed to parse prompt '${name}' response: invalid payload shape (status ${response.status} ${response.statusText}).`,
);
}
return parsed;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/prompts.ts` at line 409, The unchecked cast "parsed
as RemotePrompt" must be replaced with runtime validation: implement a type
guard function (e.g., isRemotePrompt(obj): obj is RemotePrompt) that checks
required fields/types of the RemotePrompt interface and use it before returning;
in the function that currently does "return parsed as RemotePrompt" call
isRemotePrompt(parsed) and throw/handle an error if it returns false so only
validated RemotePrompt objects are returned. Ensure the type guard inspects all
unique properties used elsewhere (names/types referenced by RemotePrompt) and
update callers to rely on the guard rather than the direct cast.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 3 files

@calebcgates
Copy link
Copy Markdown
Author

calebcgates commented May 13, 2026

Quick response to the review from code rabbit:

JSON.stringify in the spec: I left this as-is. Looking around @voltagent/cli, JSON.stringify is used in mcp-config.ts, init.ts, login.ts:102 (HTTP body), mcp.ts, dataset-loader.ts, and tunnel.ts:265,282 (HTTP bodies — the same shape my test mocks), without any biome rule or CONTRIBUTING note flagging it. safeStringify is used in prompts.ts specifically for user-supplied prompt content where circular refs / non-serializable values are a real concern; the test payload here is a literal POJO. Happy to switch if you'd prefer consistency, but wanted to flag the asymmetry before adding a one-off.

parsed as RemotePrompt cast: Good catch — but the pre-fix code was already return (await response.json()) as RemotePrompt;, so the cast is pre-existing. I deliberately kept the scope to "guard the parse step against non-JSON bodies" rather than widen into runtime shape validation, since adding isRemotePrompt(...) changes the function's contract (validation errors vs. parse errors are different failure modes that merit their own logging shape). Happy to send a follow-up PR introducing a type guard if you'd like — it'd be a meaningful design decision worth discussing in a separate thread.

Copy link
Copy Markdown
Member

@omeraplak omeraplak left a comment

Choose a reason for hiding this comment

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

Thank you so much!!

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