Skip to content

Commit c1f9a0a

Browse files
rodion-mclaude
andcommitted
Surface RFC 9457 ProblemDetails fields to LLMs in error envelopes
Adds a 400 template plus a parser that pulls Detail, field-attributed errors[field][] / validationErrors, and requestId out of CodeAlive ProblemDetails bodies, then appends the summary to the LLM-facing error message via _enrich. Without this, agents get the bare template ("Bad request (400): ...") with no hint about which field tripped validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0ba88a4 commit c1f9a0a

1 file changed

Lines changed: 64 additions & 4 deletions

File tree

src/utils/errors.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,45 @@
1515
``codebase_consultant``) should suggest checking ``conversation_id``.
1616
"""
1717

18+
import json
1819
from dataclasses import dataclass
19-
from typing import Mapping, Optional
20+
from typing import Any, Mapping, Optional
2021

2122
import httpx
2223
from fastmcp import Context
2324
from fastmcp.exceptions import ToolError
2425

2526
from core.config import REQUEST_TIMEOUT_SECONDS
2627

28+
29+
def _parse_problem_details(body: str) -> Optional[dict[str, Any]]:
30+
"""Parse a CodeAlive RFC 9457 / legacy error body, return None on mismatch."""
31+
if not body:
32+
return None
33+
try:
34+
d = json.loads(body)
35+
except (ValueError, TypeError):
36+
return None
37+
return d if isinstance(d, dict) else None
38+
39+
40+
def _summarise_field_errors(d: dict[str, Any]) -> Optional[str]:
41+
"""Render a one-line ``field: msg; field: msg`` summary from RFC 9457
42+
``errors`` (a dict-of-lists) or the legacy flat ``validationErrors`` list."""
43+
errors = d.get("errors")
44+
if isinstance(errors, dict):
45+
rendered = [
46+
f"{field}: {msg}"
47+
for field, msgs in errors.items()
48+
for msg in (msgs or [])
49+
]
50+
if rendered:
51+
return "; ".join(rendered)
52+
legacy = d.get("validationErrors")
53+
if isinstance(legacy, list) and legacy:
54+
return "; ".join(str(x) for x in legacy)
55+
return None
56+
2757
# GitHub Issues URL is verified in README.md and manifest.json — safe to embed.
2858
_ISSUES_URL = "https://github.com/CodeAlive-AI/codealive-mcp/issues"
2959

@@ -66,6 +96,18 @@ class _ErrorTemplate:
6696

6797

6898
_ERROR_TEMPLATES: dict[int, _ErrorTemplate] = {
99+
400: _ErrorTemplate(
100+
label="Bad request (400): The CodeAlive service rejected the request",
101+
retryable=False,
102+
retry_window=None,
103+
default_hint=(
104+
"(1) inspect the field-level errors below and fix the offending parameter, "
105+
"(2) for conversation_id / message_id, ensure the value is a 24-character hex "
106+
"Mongo ObjectId taken from a previous response, "
107+
"(3) if no field errors are surfaced, re-read the tool docstring and verify "
108+
"the request shape matches"
109+
),
110+
),
69111
401: _ErrorTemplate(
70112
label="Authentication error (401): Invalid API key or insufficient permissions",
71113
retryable=False,
@@ -213,21 +255,39 @@ async def handle_api_error(
213255
error_code = error.response.status_code
214256
error_detail = error.response.text
215257

258+
# Parse the CodeAlive RFC 9457 / legacy error body once. The summary is
259+
# appended to whichever branch handles this status code, so the LLM sees
260+
# field-level errors and the requestId without scanning raw JSON.
261+
problem = _parse_problem_details(error_detail)
262+
field_summary = _summarise_field_errors(problem) if problem else None
263+
request_id = problem.get("requestId") if problem else None
264+
rfc_detail = problem.get("detail") if problem else None
265+
266+
def _enrich(msg: str) -> str:
267+
extras: list[str] = []
268+
if rfc_detail and rfc_detail not in msg:
269+
extras.append(f"Detail: {rfc_detail}")
270+
if field_summary:
271+
extras.append(f"Fields: {field_summary}")
272+
if request_id:
273+
extras.append(f"requestId={request_id}")
274+
return msg if not extras else f"{msg} ({' | '.join(extras)})"
275+
216276
template = _ERROR_TEMPLATES.get(error_code)
217277
if template is not None:
218278
hint = (recovery_hints or {}).get(error_code, template.default_hint)
219-
error_msg = _format_error(template, hint)
279+
error_msg = _enrich(_format_error(template, hint))
220280
elif error_code >= 500:
221281
# Unknown 5xx — treat as retryable server error
222-
error_msg = (
282+
error_msg = _enrich(
223283
f"Server error ({error_code}): The CodeAlive service encountered an issue. "
224284
"Retry: yes (retry once after a few seconds). "
225285
"Try: (1) retry the call once, "
226286
f"(2) report a persistent error at {_ISSUES_URL}"
227287
)
228288
else:
229289
# Unknown 4xx — keep raw detail (truncated) and a conservative hint
230-
error_msg = (
290+
error_msg = _enrich(
231291
f"HTTP error: {error_code} - {error_detail[:200]}. "
232292
"Retry: no — fix the input. "
233293
"Try: review the parameters you passed and try a different value"

0 commit comments

Comments
 (0)