Skip to content

fix: global extension bugs — list_extensions, catch-all handler, tests#902

Merged
PythonFZ merged 8 commits intomainfrom
fix/global-extension-bugs
Apr 1, 2026
Merged

fix: global extension bugs — list_extensions, catch-all handler, tests#902
PythonFZ merged 8 commits intomainfrom
fix/global-extension-bugs

Conversation

@PythonFZ
Copy link
Copy Markdown
Member

@PythonFZ PythonFZ commented Apr 1, 2026

Summary

  • fix: list_extensions() defaults to actual room instead of @internal@global and room-scoped extensions were hidden from the Python client and CLI because APIManager.list_extensions() hardcoded @internal as fallback. Now uses self.room_id so the server returns all visible extensions (matching frontend behavior).
  • feat: catch-all exception handler returning RFC 9457 — Unhandled exceptions previously returned bare Internal Server Error plain text with no diagnostics. Now logs full traceback server-side and returns application/problem+json via new InternalServerError problem type. Also refactors _validation_exception_handler to delegate to problem_exception_handler instead of building its own JSONResponse.
  • test: regression test for frames with nested dict+numpy in atoms.info — The Isosurface feature depends on storing nested dicts with numpy arrays in frame info. This test ensures the append+read round-trip works.
  • docs: clarify JobManager.register() room default — Docstring said "Defaults to @global" but ZnDraw.register_job() resolves None to the client's room.

Test plan

  • uv run pytest tests/zndraw/test_problems.py — 4 passed
  • uv run pytest tests/zndraw/test_routes_frames.py — all passed (including new nested dict test)
  • uv run pytest tests/zndraw/test_cli_agent/test_extensions.py — all passed
  • uv run pytest tests/zndraw/worker/test_global.py — all passed (including new list_extensions test)
  • Full run: 70 passed, 0 failed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed extension listing to correctly include global extensions when no room is specified.
    • Ensured unhandled server errors return standardized RFC 9457 problem responses (500 Internal Server Error).
  • Documentation

    • Clarified default room semantics in job registration docs.
    • Added design/spec document describing global-extension fixes and 500-response behavior.
  • Tests

    • Added tests covering global extension discovery, 500 problem docs, and nested-frame round-trip for complex metadata.

PythonFZ and others added 6 commits April 1, 2026 09:24
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The APIManager.list_extensions() method defaulted to querying
@internal when no room was specified, hiding @global and room-scoped
extensions. Now falls back to self.room_id so the server returns
all visible extensions (global + internal + room-scoped).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add InternalServerError import and catch-all @app.exception_handler(Exception)
- Logs full traceback server-side, returns application/problem+json
- Refactor _validation_exception_handler to delegate to problem_exception_handler
  instead of building its own JSONResponse with model_dump

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_list_extensions_includes_global: verify list_extensions() returns @global extensions
- test_unhandled_exception_returns_problem_json: verify InternalServerError is registered
- test_append_frame_with_nested_info_dict: regression test for frames with nested dict+numpy in atoms.info

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e430a3d3-54e0-4ac1-ac01-bb722e99b464

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6ee00 and 110e77c.

📒 Files selected for processing (4)
  • docs/superpowers/specs/2026-04-01-global-extension-bugs-design.md
  • src/zndraw/app.py
  • tests/zndraw/test_problems.py
  • tests/zndraw/test_routes_frames.py
✅ Files skipped from review due to trivial changes (3)
  • tests/zndraw/test_problems.py
  • tests/zndraw/test_routes_frames.py
  • docs/superpowers/specs/2026-04-01-global-extension-bugs-design.md

📝 Walkthrough

Walkthrough

Adds an RFC 9457 InternalServerError problem type and global FastAPI exception handler (returns problem+json and logs trace), fixes APIManager.list_extensions() to default to the client's room_id so @global extensions are discoverable, and adds tests and docs for these changes.

Changes

Cohort / File(s) Summary
Error handling & problem types
src/zndraw/exceptions.py, src/zndraw/app.py
Added InternalServerError ProblemType (status 500) and registered it; refactored RequestValidationError handling to delegate to problem_exception_handler; added a catch-all Exception handler that logs method/path/traceback and returns an RFC 9457 problem response.
API behavior fix
src/zndraw/client/api.py
APIManager.list_extensions(room: None) now uses self.room_id as the default job-room instead of hardcoded "@internal", allowing discovery of @global extensions.
Joblib docs update
src/zndraw_joblib/client.py
Clarified JobManager.register() docstring about room defaults and how ZnDraw.register_job() resolves None to the client room.
Tests added/extended
tests/zndraw/test_problems.py, tests/zndraw/test_routes_frames.py, tests/zndraw/worker/test_global.py
New tests: problem-doc endpoint includes InternalServerError; frame append/retrieve round-trip preserves nested dicts with numpy arrays; list_extensions() (no room arg) returns @global-registered jobs.
Design & plan docs
docs/superpowers/plans/2026-04-01-global-extension-bugs.md, docs/superpowers/specs/2026-04-01-global-extension-bugs-design.md
Added design/spec and plan documentation describing global-extension bugs, proposed fixes, and test plans.

Sequence Diagram(s)

(omitted)

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Poem

🐰 I hopped through logs and traced the stack,
Found stray rooms and pulled them back,
A 500 now wears a proper name,
Global jobs show up the same,
Tests nibble bugs — hooray! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly identifies the main changes: fixing global extension bugs related to list_extensions, adding a catch-all handler, and including tests. It is specific and directly reflects the primary objectives of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/global-extension-bugs

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 and usage tips.

The catch-all exception handler receives `exc` from FastAPI, not from
an `except` block, so ruff LOG014 is a false positive. Renamed test to
accurately reflect it checks problem type registration, not handler
behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
tests/zndraw/test_problems.py (1)

53-61: Exercise the actual exception path here.

This only checks the documentation page for /v1/problems/internal-server-error; _unhandled_exception_handler() could regress and this test would still pass. Please hit a route that really raises and assert the 500 application/problem+json body.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/zndraw/test_problems.py` around lines 53 - 61, The current test only
checks the documentation endpoint and can miss regressions in the actual
handler; update or add a test (e.g., in
test_unhandled_exception_returns_problem_json) that calls a real route which
raises an unhandled exception (create or reuse a test-only endpoint that raises,
or trigger the existing handler that throws) and assert the response has HTTP
500, Content-Type "application/problem+json", and a problem JSON body containing
"title": "Internal Server Error" and "status": 500; ensure this exercises the
framework handler (_unhandled_exception_handler) rather than the docs page.
tests/zndraw/test_routes_frames.py (1)

921-961: Assert the nested cube_data payload, not just the top-level key.

Right now a serializer that stores an empty or malformed info.cube_data value would still pass. Please unpack frames[0][b"info.cube_data"] and verify the nested keys/shapes or representative values so the Isosurface round-trip is actually covered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/zndraw/test_routes_frames.py` around lines 921 - 961, The test
test_append_frame_with_nested_info_dict currently only asserts the presence of
the top-level key b"info.cube_data" but not its contents; modify the test to
unpack frames[0][b"info.cube_data"] (after decode_msgpack_response) and assert
the nested payload contains expected keys and representative values/types — e.g.
verify presence of "grid","origin","cell", that "grid" has the expected shape
(10x10x10) or length, "origin" has three elements, and "cell" is a 3x3 array (or
appropriate shapes/types) to ensure the numpy arrays round-trip correctly from
atoms.info["cube_data"] through atoms_to_json_dict and the frames endpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/plans/2026-04-01-global-extension-bugs.md`:
- Line 166: The fenced code block currently missing a language identifier should
be updated to include the appropriate language tag (e.g., ```bash, ```js,
```json, etc.) so markdownlint MD040 is satisfied; locate the fenced block that
lacks a language specifier and add the correct language identifier matching the
block's contents.
- Around line 205-219: The handler _unhandled_exception_handler currently
returns InternalServerError.exception(detail=str(exc)), which can leak sensitive
internals; change it to return a generic, client-safe detail (e.g., "Internal
server error" or similar) while keeping the full exception/traceback in logs via
logger.error(exc_info=True). Update the call to
problem_exception_handler/InternalServerError.exception so the detail is the
generic message and do not include str(exc) in the response payload; keep
logging the actual exc via logger.error for debugging.
- Around line 241-259: The test test_unhandled_exception_returns_problem_json in
tests/zndraw/test_problems.py only hits the problems documentation endpoint and
doesn't exercise the actual catch-all exception handler
(`@app.exception_handler`(Exception)); update the test to call a real endpoint
that raises an unhandled Exception (or inject/mock a handler to raise) so the
request triggers the global exception handler at runtime, then assert the
response status_code is 500, the Content-Type is application/problem+json, and
the JSON body contains the expected problem fields (status: 500, title:
"Internal Server Error" and any problem-type identifier like
"internal-server-error"); keep or separately assert that the registry endpoint
(/v1/problems/internal-server-error) still returns the docs if needed.

In `@docs/superpowers/specs/2026-04-01-global-extension-bugs-design.md`:
- Around line 166-170: Add a language tag "text" to the fenced block containing
the "room" description so the opening fence becomes ```text; this fixes
markdownlint MD040 for the block that reads: "room\n    Room scope. Defaults to
``\"@global\"`` when called directly.\n    ``ZnDraw.register_job()`` resolves
*None* to the client's room." — update that specific fenced code block
surrounding the "room" paragraph to start with ```text and keep the existing
content unchanged.

In `@src/zndraw/app.py`:
- Around line 75-89: The handler _unhandled_exception_handler should not echo
raw exception text to clients; keep detailed trace in logs but return a generic
500 detail. Update the return to call problem_exception_handler(request,
InternalServerError.exception(detail="Internal server error")) (or similar
generic message) instead of using str(exc), while leaving the logger.error(...,
exc_info=True) unchanged so full traceback remains in logs; ensure the function
still returns the JSONResponse from problem_exception_handler.

---

Nitpick comments:
In `@tests/zndraw/test_problems.py`:
- Around line 53-61: The current test only checks the documentation endpoint and
can miss regressions in the actual handler; update or add a test (e.g., in
test_unhandled_exception_returns_problem_json) that calls a real route which
raises an unhandled exception (create or reuse a test-only endpoint that raises,
or trigger the existing handler that throws) and assert the response has HTTP
500, Content-Type "application/problem+json", and a problem JSON body containing
"title": "Internal Server Error" and "status": 500; ensure this exercises the
framework handler (_unhandled_exception_handler) rather than the docs page.

In `@tests/zndraw/test_routes_frames.py`:
- Around line 921-961: The test test_append_frame_with_nested_info_dict
currently only asserts the presence of the top-level key b"info.cube_data" but
not its contents; modify the test to unpack frames[0][b"info.cube_data"] (after
decode_msgpack_response) and assert the nested payload contains expected keys
and representative values/types — e.g. verify presence of
"grid","origin","cell", that "grid" has the expected shape (10x10x10) or length,
"origin" has three elements, and "cell" is a 3x3 array (or appropriate
shapes/types) to ensure the numpy arrays round-trip correctly from
atoms.info["cube_data"] through atoms_to_json_dict and the frames endpoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 428ae66a-6029-45d2-b8be-faccad32d076

📥 Commits

Reviewing files that changed from the base of the PR and between 38db250 and 9b6ee00.

📒 Files selected for processing (9)
  • docs/superpowers/plans/2026-04-01-global-extension-bugs.md
  • docs/superpowers/specs/2026-04-01-global-extension-bugs-design.md
  • src/zndraw/app.py
  • src/zndraw/client/api.py
  • src/zndraw/exceptions.py
  • src/zndraw_joblib/client.py
  • tests/zndraw/test_problems.py
  • tests/zndraw/test_routes_frames.py
  • tests/zndraw/worker/test_global.py

- [ ] **Step 3: Refactor `_validation_exception_handler` to delegate**

Replace the existing `_validation_exception_handler` (lines 58-71):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit language to the fenced code block at Line 166

markdownlint flags this as MD040. Add a language identifier to keep docs lint-clean.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 166-166: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-01-global-extension-bugs.md` at line 166, The
fenced code block currently missing a language identifier should be updated to
include the appropriate language tag (e.g., ```bash, ```js, ```json, etc.) so
markdownlint MD040 is satisfied; locate the fenced block that lacks a language
specifier and add the correct language identifier matching the block's contents.

Comment on lines +205 to +219
@app.exception_handler(Exception)
async def _unhandled_exception_handler(
request: Request, exc: Exception
) -> JSONResponse:
"""Catch-all for unhandled exceptions — log and return RFC 9457."""
logger.error(
"Unhandled %s on %s %s",
type(exc).__name__,
request.method,
request.url.path,
exc_info=True,
)
return await problem_exception_handler(
request, InternalServerError.exception(detail=str(exc))
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid returning raw exception messages in 500 responses

At Line 218, using detail=str(exc) can leak sensitive internal data to clients. Keep traceback/details in logs and return a generic client-safe detail in the RFC 9457 payload.

Safer handler snippet
     return await problem_exception_handler(
-        request, InternalServerError.exception(detail=str(exc))
+        request,
+        InternalServerError.exception(
+            detail="An unexpected error occurred while processing the request."
+        ),
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-01-global-extension-bugs.md` around lines 205
- 219, The handler _unhandled_exception_handler currently returns
InternalServerError.exception(detail=str(exc)), which can leak sensitive
internals; change it to return a generic, client-safe detail (e.g., "Internal
server error" or similar) while keeping the full exception/traceback in logs via
logger.error(exc_info=True). Update the call to
problem_exception_handler/InternalServerError.exception so the detail is the
generic message and do not include str(exc) in the response payload; keep
logging the actual exc via logger.error for debugging.

Comment on lines +241 to +259
- [ ] **Step 1: Write the test**

Add at the end of `tests/zndraw/test_problems.py`:

```python
@pytest.mark.asyncio
async def test_unhandled_exception_returns_problem_json(client: AsyncClient) -> None:
"""Unhandled exceptions return application/problem+json with status 500."""
# Hit a URL that doesn't exist as a route — FastAPI returns 404.
# Instead, trigger via an invalid room ID that passes URL parsing
# but causes an internal error. We verify the catch-all by checking
# that the registry now includes internal-server-error.
response = await client.get("/v1/problems/internal-server-error")
assert response.status_code == 200
content = response.text
assert "# InternalServerError" in content
assert "**Status:** 500" in content
assert "**Title:** Internal Server Error" in content
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Task 5 test does not actually verify the catch-all exception handler

At Line 247-259, the test only verifies the problem-type docs endpoint (/v1/problems/internal-server-error, status 200). It does not exercise @app.exception_handler(Exception) or assert a 500 application/problem+json runtime response, so the planned regression can pass even if the catch-all handler is broken.

Proposed plan adjustment
-async def test_unhandled_exception_returns_problem_json(client: AsyncClient) -> None:
-    """Unhandled exceptions return application/problem+json with status 500."""
-    response = await client.get("/v1/problems/internal-server-error")
-    assert response.status_code == 200
-    content = response.text
-    assert "# InternalServerError" in content
-    assert "**Status:** 500" in content
-    assert "**Title:** Internal Server Error" in content
+async def test_unhandled_exception_returns_problem_json(client: AsyncClient) -> None:
+    """Unhandled exceptions return application/problem+json with status 500."""
+    response = await client.get("/v1/test/raise-unhandled")
+    assert response.status_code == 500
+    assert response.headers["content-type"].startswith("application/problem+json")
+    body = response.json()
+    assert body["status"] == 500
+    assert body["title"] == "Internal Server Error"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-01-global-extension-bugs.md` around lines 241
- 259, The test test_unhandled_exception_returns_problem_json in
tests/zndraw/test_problems.py only hits the problems documentation endpoint and
doesn't exercise the actual catch-all exception handler
(`@app.exception_handler`(Exception)); update the test to call a real endpoint
that raises an unhandled Exception (or inject/mock a handler to raise) so the
request triggers the global exception handler at runtime, then assert the
response status_code is 500, the Content-Type is application/problem+json, and
the JSON body contains the expected problem fields (status: 500, title:
"Internal Server Error" and any problem-type identifier like
"internal-server-error"); keep or separately assert that the registry endpoint
(/v1/problems/internal-server-error) still returns the docs if needed.

Comment on lines +75 to 89
@app.exception_handler(Exception)
async def _unhandled_exception_handler(
request: Request, exc: Exception
) -> JSONResponse:
"""Catch-all for unhandled exceptions — log and return RFC 9457."""
logger.error(
"Unhandled %s on %s %s",
type(exc).__name__,
request.method,
request.url.path,
exc_info=True,
)
return await problem_exception_handler(
request, InternalServerError.exception(detail=str(exc))
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't echo raw exception messages back to clients.

str(exc) can expose internal details such as SQL errors, file paths, or other implementation state. Keep the traceback in the server log, but return a generic 500 detail instead.

🛡️ Suggested change
     return await problem_exception_handler(
-        request, InternalServerError.exception(detail=str(exc))
+        request,
+        InternalServerError.exception(
+            detail="An unexpected error occurred while processing the request."
+        ),
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@app.exception_handler(Exception)
async def _unhandled_exception_handler(
request: Request, exc: Exception
) -> JSONResponse:
"""Catch-all for unhandled exceptions — log and return RFC 9457."""
logger.error(
"Unhandled %s on %s %s",
type(exc).__name__,
request.method,
request.url.path,
exc_info=True,
)
return await problem_exception_handler(
request, InternalServerError.exception(detail=str(exc))
)
`@app.exception_handler`(Exception)
async def _unhandled_exception_handler(
request: Request, exc: Exception
) -> JSONResponse:
"""Catch-all for unhandled exceptions — log and return RFC 9457."""
logger.error(
"Unhandled %s on %s %s",
type(exc).__name__,
request.method,
request.url.path,
exc_info=True,
)
return await problem_exception_handler(
request,
InternalServerError.exception(
detail="An unexpected error occurred while processing the request."
),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zndraw/app.py` around lines 75 - 89, The handler
_unhandled_exception_handler should not echo raw exception text to clients; keep
detailed trace in logs but return a generic 500 detail. Update the return to
call problem_exception_handler(request,
InternalServerError.exception(detail="Internal server error")) (or similar
generic message) instead of using str(exc), while leaving the logger.error(...,
exc_info=True) unchanged so full traceback remains in logs; ensure the function
still returns the JSONResponse from problem_exception_handler.

Add language tag to unfenced code block in design spec. Assert nested
cube_data keys (grid, origin, cell) exist after round-trip, not just
the top-level key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.07843% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.65%. Comparing base (38db250) to head (110e77c).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/zndraw/app.py 71.42% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #902      +/-   ##
==========================================
- Coverage   91.72%   91.65%   -0.07%     
==========================================
  Files         245      245              
  Lines       22791    22839      +48     
==========================================
+ Hits        20904    20933      +29     
- Misses       1887     1906      +19     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PythonFZ PythonFZ merged commit aa050bf into main Apr 1, 2026
6 checks passed
@PythonFZ PythonFZ deleted the fix/global-extension-bugs branch April 1, 2026 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants