Skip to content

fix: surface spend-precheck denial as RATE_LIMITED - #3749

Merged
ignaciojimenezr merged 7 commits into
MCPJam:mainfrom
Christian-Sidak:fix/issue-3708
Aug 7, 2026
Merged

fix: surface spend-precheck denial as RATE_LIMITED#3749
ignaciojimenezr merged 7 commits into
MCPJam:mainfrom
Christian-Sidak:fix/issue-3708

Conversation

@Christian-Sidak

@Christian-Sidak Christian-Sidak commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

When the Convex /stream endpoint denies a model call via the spend-precheck it returns 200 OK with Content-Type: application/json and a body like {ok:false, code:"user_rate_limit", isRetryable:true, retryAfter:N}. Two seams in the engine+route pipeline silently swallowed this, producing an empty-reply success envelope instead of a proper error:

  • processOneStep only checked !res.ok || !res.body, so a 200 OK JSON body fell through to processStream, which found no SSE events and returned empty contentParts. The agentic loop completed with runSucceeded=true.
  • agent.ts checked !result.turnTrace to detect failures, but because runSucceeded was true, onConversationComplete fired and set capturedTurnTrace, so result.turnTrace was defined even though onEngineError had fired with the denial payload.

Fix

  • In processOneStep (mcpjam-stream-handler.ts), detect Content-Type: application/json on a 200 OK response as a non-stream denial and route it through the same error path as non-OK responses. onEngineError now fires with the parsed body and httpStatus:200.
  • In agent.ts, check lastEngineError alongside !result.turnTrace when deciding whether to return an error. The rateLimitCodes set covers both the httpStatus === 429 path and the 200 OK path where code is "user_rate_limit" or "org_rate_limit".

Test plan

  • New engine-level test in mcpjam-stream-handler.test.ts: feeds a 200 OK + application/json response, asserts onEngineError fires with httpStatus:200 and the raw body.
  • New route-level test in agent.test.ts: mocks onEngineError with code:"user_rate_limit" and httpStatus:200, asserts the route returns 429 RATE_LIMITED.
  • All 108 existing tests in those two files continue to pass.

Fixes #3708


Summary by cubic

Maps Convex /stream spend-precheck denials (200 OK + application/json) to 429 RATE_LIMITED instead of an empty 200. Fixes #3708 so clients consistently see rate limit errors.

  • Bug Fixes
    • In mcpjam-stream-handler.ts, treat 200 OK with application/json as a non-stream denial; call onEngineError with parsed body and httpStatus: 200. Guard res.headers in the content-type check to avoid crashes with stubbed responses.
    • In agent.ts, also branch on lastEngineError (even when turnTrace exists) and map user_rate_limit/org_rate_limit to 429.
    • In parseEngineErrorBody, surface top-level code from JSON bodies without an error field so rate-limit mapping works reliably.

Written for commit 3c001e0. Summary will update on new commits.

Review in cubic

When the Convex /stream endpoint denies a model call via spend-precheck
it returns 200 OK with Content-Type: application/json and a body like
{ok:false, code:"user_rate_limit", isRetryable:true, retryAfter:N}.

Two seams failed to catch this:

1. processOneStep in mcpjam-stream-handler.ts only checked !res.ok or
   !res.body, so a 200 OK JSON body passed through to processStream.
   processStream saw no SSE events, produced empty contentParts, and the
   agentic loop completed "successfully" with runSucceeded=true.

2. agent.ts checked !result.turnTrace to detect engine failures, but
   because runSucceeded was true, onConversationComplete fired and set
   capturedTurnTrace -- so result.turnTrace was defined even though
   onEngineError had fired with the denial payload.

Fix:

- In processOneStep, treat a 200 OK with Content-Type: application/json
  as a non-stream denial (same error path as non-OK responses).
  onEngineError fires with the parsed body and httpStatus:200.

- In agent.ts, check lastEngineError in addition to !result.turnTrace
  when deciding whether to return an error. A precheck denial fires
  onEngineError with code:"user_rate_limit", which also now maps to
  RATE_LIMITED via the rateLimitCodes set (covering both the 429-status
  and the 200-with-JSON-code paths).

Tests: new cases in mcpjam-stream-handler.test.ts (engine-level) and
agent.test.ts (route-level RATE_LIMITED mapping).

Fixes MCPJam#3708

Signed-off-by: Christian Sidak <christian@sentineltech.eu>
Signed-off-by: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. bug Something isn't working labels Aug 6, 2026
@chelojimenez

chelojimenez commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dc59ea14-c95b-4807-9983-b6d7c33c822d

📥 Commits

Reviewing files that changed from the base of the PR and between e8af35d and 9c75aa4.

📒 Files selected for processing (1)
  • mcpjam-inspector/server/utils/mcpjam-stream-handler.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcpjam-inspector/server/utils/mcpjam-stream-handler.ts

Walkthrough

The stream handler now treats HTTP 200 JSON responses as denial errors and preserves their code, status, body, message, prompt, and step data. The agent route now fails turns that contain recorded engine errors, even when a trace exists. It maps user_rate_limit and org_rate_limit errors to HTTP 429 with RATE_LIMITED. Regression tests cover stream handling and agent responses.


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.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcpjam-inspector/server/routes/v1/agent.ts (1)

1141-1161: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve spend-precheck retry metadata in the RATE_LIMITED response.

onEngineError currently only accepts message, code, and httpStatus, so { code: "user_rate_limit", retryAfter: 60, ... } from the 200 OK JSON denial is stored and emitted without retryAfter. Store that metadata, enrich lastEngineError with it, and return it in the RATE_LIMITED details before calling v1Error. Also cover this in the spend-precheck test assertions.

🤖 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 `@mcpjam-inspector/server/routes/v1/agent.ts` around lines 1141 - 1161, Update
mcpjam-inspector/server/routes/v1/agent.ts:1141-1161 and the related
error-handling flow around onEngineError to retain retryAfter and other
spend-precheck metadata, enrich lastEngineError, and include that metadata in
RATE_LIMITED errorDetails before calling v1Error. Update assertions in
mcpjam-inspector/server/utils/__tests__/mcpjam-stream-handler.test.ts:3001-3012
and mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts:412-429 to verify
retryAfter is preserved in the response.
🤖 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.

Outside diff comments:
In `@mcpjam-inspector/server/routes/v1/agent.ts`:
- Around line 1141-1161: Update
mcpjam-inspector/server/routes/v1/agent.ts:1141-1161 and the related
error-handling flow around onEngineError to retain retryAfter and other
spend-precheck metadata, enrich lastEngineError, and include that metadata in
RATE_LIMITED errorDetails before calling v1Error. Update assertions in
mcpjam-inspector/server/utils/__tests__/mcpjam-stream-handler.test.ts:3001-3012
and mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts:412-429 to verify
retryAfter is preserved in the response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d5561c91-1b03-4864-996a-d11f4d341ad7

📥 Commits

Reviewing files that changed from the base of the PR and between 98db5cc and e15810c.

📒 Files selected for processing (4)
  • mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts
  • mcpjam-inspector/server/routes/v1/agent.ts
  • mcpjam-inspector/server/utils/__tests__/mcpjam-stream-handler.test.ts
  • mcpjam-inspector/server/utils/mcpjam-stream-handler.ts

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread mcpjam-inspector/server/utils/mcpjam-stream-handler.ts
Comment thread mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts Outdated
… fixes)

parseEngineErrorBody only extracted `code` when the body had an `error`
field, but the spend-precheck denial is {ok:false, code:"user_rate_limit",
...} — so the agent route's new rateLimitCodes check never saw a code and
the 429 mapping only worked because the raw body text embedded in the
fallback message happened to match classifyFailure's rate-limit regex.
Now the top-level `code` is surfaced even without `error`.

Also tighten both new tests so they exercise the fix:
- engine test asserts event.code === "user_rate_limit"
- route test keeps turnTrace PRESENT (the actual MCPJam#3708 scenario — the old
  !turnTrace check alone never fired) and uses a message the regex cannot
  catch, so it fails without the lastEngineError + code-set branches

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Aug 6, 2026
ignaciojimenezr and others added 5 commits August 6, 2026 18:30
`res.headers.get(...)` optional-chains the RESULT of `.get`, not `headers`
itself. Callers that stub a Response without `headers` — the eval runner's
`backendStreamResponse()` returns {ok, status, statusText, body, text} — hit
a TypeError that the outer catch turns into a failed turn, breaking 7 tests
in evals-runner.test.ts and runner-parity.test.ts against current main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ignaciojimenezr
ignaciojimenezr merged commit db608c4 into MCPJam:main Aug 7, 2026
11 checks passed
@ignaciojimenezr

Copy link
Copy Markdown
Collaborator

hey @Christian-Sidak, thanks for fixing this! Added a commit on top and merging! Anything else we can help you with?

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

Labels

bug Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agent: /stream spend-precheck denial surfaces as an empty 200 turn, not RATE_LIMITED

3 participants