fix: handleMessage avoids [object Object] errors and enforces valid JSON-RPC error codes for thrown plain objects#40715
Conversation
|
Hey A couple of things to address before this is ready for review:
If you'd like a hand getting started:
|
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…ndleMessage
The catch block in handleMessage used `String(e)` for non-Error thrown values,
producing `[object Object]` instead of the actual validation message. This caused
submit_pull_request_review calls to fail with -32603 `[object Object]` instead of
the descriptive -32602 validation error when safe_outputs_handlers.cjs threw
plain objects like { code: -32602, message: '...' }.
Align handleMessage with the existing handleRequest pattern: extract the error
code from the thrown value (requiring it to be negative per JSON-RPC spec) and
use e.message when available.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes stdio-transport MCP error responses so that when a tool handler throws a plain object (e.g., validation errors), the JSON-RPC response includes the intended error.code and error.message instead of -32603 [object Object].
Changes:
- Updated
handleMessage(stdio path) to propagate a negative JSON-RPC errorcodefrom thrown objects and prefere.messagewhen present. - Added tests covering thrown plain-object errors with and without an explicit error code.
- Introduced whitespace-only changes in an auto-generated maintenance workflow file (should be reverted).
Show a summary per file
| File | Description |
|---|---|
| actions/setup/js/mcp_server_core.cjs | Adjusts stdio error handling to extract JSON-RPC error code/message from non-Error thrown values. |
| actions/setup/js/mcp_server_core.test.cjs | Adds regression tests ensuring thrown plain-object errors return the correct code/message and avoid [object Object]. |
| .github/workflows/agentics-maintenance.yml | Contains whitespace-only edits (trailing-space # lines) in an auto-generated workflow header section. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/3 changed files
- Comments generated: 4
| // Use the error code only if it's a valid JSON-RPC error code (must be a negative integer). | ||
| // Subprocess exit codes (positive integers like 1, 2, etc.) must not be used as JSON-RPC | ||
| // error codes, as that would produce non-conformant responses (e.g. "code=1"). | ||
| const code = e && typeof e.code === "number" && e.code < 0 ? e.code : -32603; |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #40715 does not have the 'implementation' label and has 0 new lines of code in business logic directories (≤100 threshold). 3 files changed. |
|
✅ PR Code Quality Reviewer completed the code quality review. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ Test Quality Sentinel completed test quality analysis. |
|
test body
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 80/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). Both new tests verify observable JSON-RPC error responses when a handler throws a plain object, directly covering the behavioral contract fixed in this PR. Note: a test inflation flag (49 test lines / 5 prod lines = 9.8×) deducted 10 pts but is non-blocking — the verbosity is justified by full MCP request/response cycle scaffolding.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnose and /tdd — approving with minor suggestions.
📋 Key Themes & Highlights
Key Themes
- Root cause correctly addressed: The
String(e)coercion in the oldhandleMessagecatch block was the real bug; the fix extractse.codeande.messagedirectly, mirroring the already-correcthandleRequestpath. - Positive-code guard is important but untested: The code comment explicitly warns that subprocess exit codes (positive integers) must not be forwarded as JSON-RPC error codes. A third test case would lock in this behaviour.
- Subtle transport asymmetry:
handleRequestfalls back to"Internal error"for messageless throws;handleMessagenow falls back toString(e). Both are defensible, but the divergence should be intentional.
Positive Highlights
- ✅ Fix is a surgical one-liner that correctly aligns
handleMessagewithhandleRequest - ✅ Two targeted regression tests reproduce both failure modes before asserting the fix
- ✅ Null guard (
e && ...) on the code extraction is a nice defensive addition - ✅ The code comment clearly explains why positive codes are rejected — good documentation
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 78.8 AIC · ⌖ 10.4 AIC · ⊞ 6.5K
| // Subprocess exit codes (positive integers like 1, 2, etc.) must not be used as JSON-RPC | ||
| // error codes, as that would produce non-conformant responses (e.g. "code=1"). | ||
| const code = e && typeof e.code === "number" && e.code < 0 ? e.code : -32603; | ||
| server.replyError(id, code, e && e.message ? String(e.message) : String(e)); |
There was a problem hiding this comment.
[/diagnose] The message fallback here (String(e)) diverges from how handleRequest handles the same situation (err.message || "Internal error"). For primitives like throw 42 the stdio path returns "42" while the HTTP path returns "Internal error" — a subtle per-transport inconsistency that can complicate debugging.
💡 Suggested alignment
handleRequest (line 840):
message: err.message || "Internal error",handleMessage (this line):
e && e.message ? String(e.message) : String(e)Consider aligning both paths to the same fallback message, or add a comment explaining why the two transports intentionally differ. Either choice is fine, but the asymmetry should be deliberate.
There was a problem hiding this comment.
Non-blocking but fix is incomplete and the key guard has no test
The core bug ([object Object] in error responses for plain-object throws) is correctly fixed for the primary case (safe_outputs_handlers.cjs throwing { code, message }). Two issues prevent a clean approve:
⚠️ Issues found
1. Message fallback still produces [object Object] (line 991) — String(e) fires any time e.message is falsy, including absent or empty-string. The reference implementation (handleRequest) avoids this entirely with err.message || "Internal error". The stdio path should match.
2. Positive-code suppression untested (test file, line 521) — The comment on lines 987–989 explicitly calls out blocking positive subprocess exit codes as the motivation for e.code < 0. No test exercises throw { code: 1, message: "..." } → code should be -32603. If the guard is ever inverted or removed, nothing catches it.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
patchdiff.githubusercontent.com
To allow these domains, add them to the
network.allowedlist in your workflow frontmatter:
network:
allowed:
- defaults
- "patchdiff.githubusercontent.com"See Network Configuration for more information.
🔎 Code quality review by PR Code Quality Reviewer · 109.1 AIC · ⌖ 7.39 AIC · ⊞ 5.1K
| // Subprocess exit codes (positive integers like 1, 2, etc.) must not be used as JSON-RPC | ||
| // error codes, as that would produce non-conformant responses (e.g. "code=1"). | ||
| const code = e && typeof e.code === "number" && e.code < 0 ? e.code : -32603; | ||
| server.replyError(id, code, e && e.message ? String(e.message) : String(e)); |
There was a problem hiding this comment.
Message fallback String(e) still produces [object Object] for objects without a message, diverging from handleRequest.
e && e.message ? is a truthiness check — it drops any thrown object whose message property is an empty string "" or absent, falling through to String(e) which produces [object Object] for any plain object. This is the exact symptom the PR claims to fix.
The reference implementation (handleRequest, line 840) correctly uses err.message || "Internal error", avoiding the object serialization entirely.
💡 Suggested fix
// Current (can still produce "[object Object]" when message is absent or "")
server.replyError(id, code, e && e.message ? String(e.message) : String(e));
// Aligned with handleRequest:
server.replyError(id, code, (e && e.message) ? String(e.message) : "Internal error");This matches handleRequest's err.message || "Internal error" exactly and ensures the stdio path never serializes a plain object into the message field.
|
@copilot Rub pr-finisher skill |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Ran pr-finisher flow and pushed follow-up fixes in |
Weekly update covering the week of June 15–22, 2026: - Compiler +320% performance regression fix (#40662) - New deferinloop Go linter (#40679) - gh-aw-detection rollout to 50% of workflows (#40698) - JSON-RPC error handling fix (#40715) - Skillet sparse checkout path fix (#40684) - FNV-1a heredoc delimiter generation (#40696) - Agent of the Week: delight ✨ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
submit_pull_request_reviewwas failing with-32603 [object Object]because thehandleMessagecatch block inmcp_server_core.cjsusedString(e)for non-Errorthrown values.safe_outputs_handlers.cjsthrows plain objects — notErrorinstances — for validation errors, soString({code: -32602, message: "..."})→[object Object].The HTTP path (
handleRequest) already handled this correctly; the stdio path did not.Changes
mcp_server_core.cjs— alignhandleMessagecatch withhandleRequest, and tighten JSON-RPC code handling:e.codeonly when it is a negative integer (and from an object)e.messagewhen present"Internal error"instead of serializing the thrown valuemcp_server_core.test.cjs— expanded regression coverage for thrown plain-object errors:-32603-32603-32603"Internal error"