From 192f1550517a784c4f1ce6a7bfe6b90116af70dc Mon Sep 17 00:00:00 2001 From: Mickael Farina Date: Wed, 7 Oct 2026 13:53:27 +0700 Subject: [PATCH] Show the server's reason when a page action is refused The shell's request helpers read only {error}; FastAPI's HTTPException answers {detail} (a list of {msg} for a validation error), so a refused Inbox Grant or Resume said "HTTP 400". errText() now reads error, then detail, then falls back to the status. Pages restamped for the new codec-shell.js. Co-Authored-By: Claude Opus 5.5 --- codec_chat.html | 2 +- codec_cortex.html | 2 +- codec_dashboard.html | 2 +- codec_tasks.html | 2 +- codec_vibe.html | 2 +- codec_voice.html | 2 +- docs/known-issues.md | 2 +- static/codec-shell.js | 11 ++++++++-- tests/test_shell_error_reason.py | 37 ++++++++++++++++++++++++++++++++ 9 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 tests/test_shell_error_reason.py diff --git a/codec_chat.html b/codec_chat.html index 5508f59d..95dbf4cc 100644 --- a/codec_chat.html +++ b/codec_chat.html @@ -431,7 +431,7 @@ - +
diff --git a/codec_cortex.html b/codec_cortex.html index 9ca494a4..1154fd70 100644 --- a/codec_cortex.html +++ b/codec_cortex.html @@ -182,7 +182,7 @@ - +
diff --git a/codec_dashboard.html b/codec_dashboard.html index 059390d2..1b5abc78 100644 --- a/codec_dashboard.html +++ b/codec_dashboard.html @@ -666,7 +666,7 @@ - + diff --git a/docs/known-issues.md b/docs/known-issues.md index fba080f6..b80868aa 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -411,7 +411,7 @@ same problem and was fixed on 2026-09-05 (entry above). Fix: send these through `codec_audit.log_event` like the other services. Found in the Mac merge pass of #418-#439. Revisit: the next item that touches either bridge. -## Inbox actions show "HTTP 400" instead of the reason (2026-10-07) +## Inbox actions show "HTTP 400" instead of the reason (2026-10-07) — FIXED (the shell reads {error} or {detail}) When an Inbox action is refused (for example a grant that is not allowed), the toast says "HTTP 400" and drops the server's `error` / `detail` text, so the diff --git a/static/codec-shell.js b/static/codec-shell.js index 7d37c3bb..3136beee 100644 --- a/static/codec-shell.js +++ b/static/codec-shell.js @@ -1246,11 +1246,18 @@ if (m) h['x-csrf-token'] = m[1]; return h; } + // The server's reason for a refusal: {error} from CODEC's own routes, {detail} from + // FastAPI's HTTPException (a list of {msg} for a validation error); else the status. + function errText(d, status) { + var x = d && (d.error || d.detail); + if (Array.isArray(x)) x = x.map(function (e) { return (e && e.msg) || ''; }).filter(Boolean).join('; '); + return (typeof x === 'string' && x) ? x : 'HTTP ' + status; + } function postJSON(url, data) { var h = csrf({ 'Content-Type': 'application/json' }); return fetch(url, { method: 'POST', headers: h, body: JSON.stringify(data || {}) }).then(function (r) { return r.json().catch(function () { return {}; }).then(function (d) { - if (!r.ok) throw new Error(d.error || ('HTTP ' + r.status)); + if (!r.ok) throw new Error(errText(d, r.status)); return d; }); }); @@ -1729,7 +1736,7 @@ if (!SAY.reqs[i]) { SAY.reqs[i] = fetch('/api/tts', { method: 'POST', headers: csrf({ 'Content-Type': 'application/json' }), body: JSON.stringify({ text: SAY.parts[i] }) }).then(function (r) { - if (!r.ok) return r.json().catch(function () { return {}; }).then(function (d) { throw new Error(d.error || ('HTTP ' + r.status)); }); + if (!r.ok) return r.json().catch(function () { return {}; }).then(function (d) { throw new Error(errText(d, r.status)); }); return r.blob(); }).then(function (b) { var u = URL.createObjectURL(b); diff --git a/tests/test_shell_error_reason.py b/tests/test_shell_error_reason.py new file mode 100644 index 00000000..69850c63 --- /dev/null +++ b/tests/test_shell_error_reason.py @@ -0,0 +1,37 @@ +"""A refused page action shows the server's reason, not "HTTP 400" (docs/known-issues.md). + +The shell's postJSON only read {error}; FastAPI's HTTPException answers {detail}, +so an Inbox Grant or Resume refused by the agents routes said "HTTP 400". +""" +from __future__ import annotations + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +SHELL = (Path(__file__).resolve().parent.parent / "static" / "codec-shell.js").read_text(encoding="utf-8") + + +def _err_text_source() -> str: + m = re.search(r"function errText\(d, status\) \{.*?\n \}\n", SHELL, re.S) + assert m, "errText is defined in the shell" + return m.group(0) + + +def test_every_shell_request_helper_uses_the_reason(): + assert "throw new Error(d.error || ('HTTP ' + r.status))" not in SHELL + assert SHELL.count("errText(d, r.status)") >= 2 + + +@pytest.mark.skipif(shutil.which("node") is None, reason="needs node") +def test_the_reason_comes_from_error_detail_or_a_validation_list(): + cases = [({"error": "Not allowed here."}, 400), ({"detail": "agent must be paused"}, 409), + ({"detail": [{"msg": "field required"}, {"msg": "bad value"}]}, 422), ({}, 500), (None, 502)] + js = _err_text_source() + "console.log(JSON.stringify(%s.map(c => errText(c[0], c[1]))));" % json.dumps(cases) + out = subprocess.run(["node", "-e", js], capture_output=True, text=True, timeout=30, check=True).stdout + assert json.loads(out) == ["Not allowed here.", "agent must be paused", "field required; bad value", + "HTTP 500", "HTTP 502"]