feat(errors): expose refusal_subject on RouterError / ContentPolicyViolation - #192
mattmillerai wants to merge 2 commits into
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (7)
Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Your free on-demand review promotion remains active until October 9, 2026 at 6:00 PM UTC. 📝 WalkthroughWalkthroughProtocol and Router errors now carry an optional refusal subject. Error conversion reads the value from the response header or body, applies validation, and passes it through SDK error conversion. ChangesRefusal subject handling
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant HTTPResponse
participant transport
participant error_from_envelope
participant ApiError
participant to_sdk_error
participant RouterError
HTTPResponse->>transport: error response and refusal-subject header
transport->>error_from_envelope: status, body, and header value
error_from_envelope->>ApiError: selected refusal subject
ApiError->>to_sdk_error: protocol error with refusal subject
to_sdk_error->>RouterError: refusal subject
Suggested reviewers: Merge Risk: ⚪ Minimal · up to No additional merge-blocking issue is established by this review. Previously published findings remain for the owner to consider. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 7 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 4 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
| 🟢 Low | 2 |
| ⚪ Nit | 1 |
Panel: 6/6 reviewers contributed findings.
wei-hai
left a comment
There was a problem hiding this comment.
Reviewed bounded refusal-subject parsing and propagation through synchronous, asynchronous and completion errors; no blocking findings.
…olation Read which input or output a content-policy refusal was about from the X-Comfy-Refusal-Subject header, falling back to the body's refusal_subject, and carry it as the raw wire value on RouterError.refusal_subject. REFUSAL_SUBJECTS lists the ten documented values without narrowing to them. Threaded through every surface that builds a RouterError: error_from_response, error_from_completion, and the awaited models.run path (transport -> ApiError -> to_sdk_error).
Address review: the header/body value is server-controlled and lands on a displayed attribute, so read it through a shared clean_refusal_subject (full-match, <=64 chars of [A-Za-z0-9_.-]) on both error surfaces. A duplicated header joined by httpx, control/bidi characters or an overlong value reads as undisclosed instead of being logged verbatim. Also: say REFUSAL_SUBJECT_HEADER is 'typically one of' REFUSAL_SUBJECTS, correct the queued-path comment about headers, and add a spec tripwire so a sync that declares the subject forces REFUSAL_SUBJECTS to be reconciled.
4de4c34 to
9db6df3
Compare
robinjhuang
left a comment
There was a problem hiding this comment.
Auto-approved under the full-autonomy policy.
Gates verified at 9db6df337fb3d1110b5cf70e7e93ec3a07e260fe:
full-autonomylabel present- assigned to, or review requested from, @robinjhuang
- not a draft
- 8 required check(s) green — none failing, none pending
This approval attests
that the machine gates above passed at this commit. It does not attest that a
human read the diff.
ELI-5
When the Router refuses a model call on content-policy grounds, the SDK raised
ContentPolicyViolationbut could not tell you what was refused — your prompt, your image, or the model's output. The Router can now say which one (input_image,output_text, ...). This PR reads that and puts it on the exception asexc.refusal_subject.What changed
RouterError.__init__gainsrefusal_subject: str | None = None, stored as.refusal_subject. It is never narrowed to the documented list, the same policyerror_typefollows, so a subject added after this version was built still reaches the caller. It is bounded, though: every read goes through a sharedcomfy_low.errors.clean_refusal_subject, which full-matches one token of[A-Za-z0-9_.-]{1,64}. A duplicated header (which httpx joins as"a, b"), control or bidi characters, or an overlong value reads asNoneinstead of reaching a log line verbatim. Same name as the TypeScript SDK'srefusalSubject/refusal_subject.comfy_sdk.router_exceptions:REFUSAL_SUBJECT_HEADER = "X-Comfy-Refusal-Subject"andREFUSAL_SUBJECTS, which lists the ten documented values (input,output,input_{text,image,video,audio},output_{text,image,video,audio}). Both are added to__all__.error_from_responsereads the header first (case-insensitively; a blank or malformed value counts as absent) and falls back tobody["refusal_subject"]. A non-string body value becomesNone.models.runpath does not go througherror_from_response. It goes transport →comfy_low.ApiError→comfy_sdk.exceptions.to_sdk_error. If only the factory had changed,exc.refusal_subjectwould always beNoneon the main call path. SoApiErroranderror_from_envelopenow also acceptrefusal_subject, using the same header-then-body order. The transport passes the header, andto_sdk_errorforwards the value onto theRouterError.error_from_completion(the queued surface) readsrefusal_subjectfrom the completed payload only. The poll and result responses do have headers, but only the body reaches this function, so a subject sent only on the header reads asNoneon the queued path.ServerStateknobs,model_run_error_headersandmodel_run_error_body_extra, for Router-shaped failures.[Unreleased] / Addedentry.Tests cover: header wins over body; body only; absent; blank header falls back to body; non-string body value; unknown value passes through unchanged (header and body); malformed values (duplicated, ANSI, newline, bidi, overlong) read as
Noneon the header, body and completion surfaces, and a malformed header falls back to the body; the 422detail[]validation shape never sets it; the queued completion; and full sync and asyncmodels.runruns against the stub server (header-wins, body-only, absent, hostile-header). A spec tripwire,test_the_refusal_subject_stays_undeclared_until_someone_reconciles_it, fails once a sync mentions the header or field name.Judgment calls
models.runpath and the queued completion, for the reason given above.RouterError, as the ticket asked. It is not limited toContentPolicyViolation. If the Router ever sends the header with another bucket, the value passes through rather than being dropped.Provenance
ruff check .: all checks passed;ruff format --check .: 60 files already formatted;mypy src: no issues in 22 source files;pytest: 1162 passed, 9 skipped;scripts/check_drift.py: OK;scripts/check_public_repo_hygiene.py: OK. PR CI at 9db6df3 is green on every check.Residual
spec/router-openapi.yamldoes not declarerefusal_subjectorX-Comfy-Refusal-Subjectyet. The field name, the header name and the ten values come from the upstream Router change described to this work, not from a contract in this repo. When the spec sync that adds them lands, check three things: the header spelling, the body field name, and that the enum matchesREFUSAL_SUBJECTSin the same order.test_the_refusal_subject_stays_undeclared_until_someone_reconciles_itfails at that sync so this can't be missed. Replace it with a real pin then.refusalSubject/refusal_subject), but the TypeScript SDK'srouterErrors.tswas not opened.Summary by CodeRabbit
None.