fix: guard_sync wraps TimeoutError, cancels pending future on timeout#43
fix: guard_sync wraps TimeoutError, cancels pending future on timeout#43beonde wants to merge 4 commits into
Conversation
…sage TimeoutError from concurrent.futures was leaking to users when guard_sync was called outside MCP context. Now catches it and raises GuardError with a helpful message explaining the likely cause.
|
✅ Integration tests passed! capiscio-core gRPC tests working. |
There was a problem hiding this comment.
Pull request overview
This PR improves the developer experience of the synchronous guard decorator (guard_sync) by converting timeout failures into a consistent GuardError and attempting to prevent leaked pending futures, alongside a patch version bump.
Changes:
- Wrap
future.result(timeout=30.0)timeouts inguard_sync()asGuardErrorand cancel the pending future on timeout. - Bump package version in
pyproject.tomlfrom2.7.1to2.7.2.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
capiscio_mcp/guard.py |
Adds timeout handling around run_coroutine_threadsafe(...).result() including future cancellation and a GuardError wrapper. |
pyproject.toml |
Updates the distribution version to 2.7.2. |
Comments suppressed due to low confidence (3)
capiscio_mcp/guard.py:702
- The timeout detail currently suggests the guard is being called outside an MCP request context, but in this branch the more likely cause is calling guard_sync while an event loop is already running in the current thread (future.result blocks the loop, so it can never complete). Consider updating the message to explicitly instruct using the async
@guard(or running the sync tool in a worker thread) when already in an async context.
"Guard evaluation timed out. This usually means the "
"guard is being called outside of an MCP request context "
"or the gRPC core server is not responding."
),
)
capiscio_mcp/guard.py:697
- This wraps the timeout into GuardError, but it drops the original exception context. Capturing the exception as
as eand re-raising withfrom ewould preserve the cause for debugging while still presenting a GuardError to callers.
except (TimeoutError, concurrent.futures.TimeoutError):
future.cancel()
raise GuardError(
reason=DenyReason.INTERNAL_ERROR,
detail=(
capiscio_mcp/guard.py:702
- New behavior on timeout (cancel pending future + raise GuardError) isn’t covered by tests. Consider adding a unit test that mocks
asyncio.run_coroutine_threadsafe(...).result()to raise a timeout and assertsfuture.cancel()is called andGuardErroris raised with the expected reason/detail.
try:
result = future.result(timeout=30.0)
except (TimeoutError, concurrent.futures.TimeoutError):
future.cancel()
raise GuardError(
reason=DenyReason.INTERNAL_ERROR,
detail=(
"Guard evaluation timed out. This usually means the "
"guard is being called outside of an MCP request context "
"or the gRPC core server is not responding."
),
)
| try: | ||
| result = future.result(timeout=30.0) | ||
| except (TimeoutError, concurrent.futures.TimeoutError): | ||
| future.cancel() | ||
| raise GuardError( | ||
| reason=DenyReason.INTERNAL_ERROR, | ||
| detail=( | ||
| "Guard evaluation timed out. This usually means the " | ||
| "guard is being called outside of an MCP request context " | ||
| "or the gRPC core server is not responding." |
| name = "capiscio-mcp" | ||
| version = "2.7.1" | ||
| version = "2.7.2" | ||
| description = "Trust badges for MCP tool calls - RFC-006 & RFC-007 implementation" |
There was a problem hiding this comment.
This is intentional — pyproject.toml version tracks the package distribution version (aligned with the capiscio ecosystem release), while MCP_VERSION is the internal MCP protocol version constant. They serve different purposes and are expected to diverge.
Extract timeout to named constant and interpolate into error detail so callers can see how long guard_sync waited.
|
Closing: duplicate of already-merged #41. Branch was erroneously re-pushed. |
DX Audit: guard_sync Error Handling
Discovered during developer experience audit —
guard_sync()let rawTimeoutErrorescape and leaked futures.Changes
guard_sync()wrapsTimeoutErrorasGuardErrorwith a descriptive message including the timeout value2 files, +13/-2