fix(channels): reject a non-string mention or thread_id instead of 500ing - #5977
Conversation
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix for a real 500, done in the handler's own established pattern ( [DESIGN-REVIEWED] d8c4712 |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of All checks are done — the fix is verified against the base, the helper reuse is real, and I've counted siblings of the root cause. Here is the review: First-Principles-Verdict: PASS A named 500 (unhashable JSON in dict membership) gets a coded 400 via the handler's existing helper; every item is the fix or declared. What this change shipsIntent: stop
Reuse checked: the response goes through the existing WatchSibling count for "raw body value crashes the handler": I grepped dashboard handlers for raw [FIRST-PRINCIPLES-REVIEWED] d8c4712 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
f1fddd2 to
d8105c3
Compare
…0ing `api_channel_post` validated `mention` and `thread_id` by testing them for membership in `ch.members` / `ch._msg_index`, both dicts. A JSON body whose `mention` or `thread_id` is a dict or a list therefore raised `TypeError: unhashable type` out of the handler and surfaced as a 500 instead of a 400, and a non-string-but-hashable `thread_id` (an int, a bool) was silently coerced to `None` rather than refused. Validate both fields before they are used as dict keys, matching the field contract already applied to `content` and to the agent mutation fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d8105c3 to
d8c4712
Compare
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix with a clear root cause — a non-string mention or thread_id in the channel-message body hit a dict membership lookup and raised TypeError (500); now rejected with a 400 and a machine-readable code before any mutation, hardening existing parsing without adding a new input surface. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix with a clear root cause — mention membership is a dict lookup, so an unhashable JSON value raised TypeError and surfaced as a 500; the handler now rejects a non-string mention entry or thread_id with a 400 and a distinct error code before any mutation, and the existing drop-unknown-value behaviour is unchanged. No new parse surface: the same body fields are read and used the same way. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: an unhashable mention or non-string thread_id in the channel-post body raised TypeError on the ch.members / _msg_index dict lookup and surfaced as a 500 -- both are now type-checked into a 400 before any mutation. Narrows the input space reaching ch.post; no new field is read and the membership filter is unchanged. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.
Problem / Motivation
POST /api/channels/{id}/messagesused raw JSONmentionandthread_idvalues in dictionary membership checks before validating their types. A dict or
nested list therefore raised
TypeError: unhashable typeand surfaced as a 500,while hashable wrong types such as an integer or boolean could be silently
accepted.
This is the residual identified by the Design review on #5618, which fixed the
same class of defect for
contentin this handler.Why it matters
The endpoint is used by the dashboard, gateway, agents, and scripts. A malformed
client field should produce a stable coded 400, not an opaque server error or a
successful mutation with silently changed input. Inconsistent behavior also
makes client recovery and audit logs unreliable.
What changed (motivation → approach → change)
mentionbefore any member lookup. It must be a string or an arraycontaining only strings; otherwise the handler returns coded 400
channel_message_mention_type_invalid.thread_idbefore any message lookup. A non-string returns coded 400channel_message_thread_id_type_invalid._agent_field_errorresponse helper instead ofadding duplicate response sinks.
mentionnormalizes toNone, an empty-stringthread_idremains"", and an unknown non-emptymember or message id is accepted and normalized to
None. Only wrong-typedvalues are refused.
On base
bcf2efb732fe3f20f2691bd41b1dd3f1ce00b88d, the committederror-code-baseline.jsontotals aremissing_code=1262,opaque_body=18,dynamic_status=43, and_compliant=1387. A fresh generator run on both currentmainand the final PR tree reports the same totals and_compliant=1388; thatone-site display-count drift already exists on
mainand is unchanged by thisdiff. The generated baseline is therefore left untouched rather than mixing an
unrelated refresh into this contributor change.
Open-PR overlap was audited globally. #5248 touches the same handler in a
different, much larger logic area and is harder to merge; #5977 is the
easier-first change. Other intersecting PRs only share the generated baseline,
which this final diff no longer changes. No contributor hunk is replaced.
The related raw
sourcemembership issue inhandlers/taskrunner.pybelongs toa different endpoint and provenance gate. It should be handled as a separate
small PR rather than expanding this contributor's scope.
Tests
mention, mixed and nested mentionarrays, and dict/list/int/bool
thread_id; all refuse before channel mutation.RuntimeWarning and PytestUnraisableExceptionWarning promoted to errors.
contract tests.
git diff --checkpass.used.
Manual verification
N/A — the defect is a request-body contract exercised directly with the exact
JSON shapes that previously crashed or were silently accepted.
Related Issues
Residual identified by the Design review on #5618. No separate issue was filed.
Checklist
Contribution License Agreement