refactor(webhooks): bound queue waits, route URLs through dynaconf

Addresses round-3 review feedback on PR #747:

- webhook_receiver: wrap send_stream.send() in anyio.fail_after(1.0)
  and return 503 with reason="queue full" if the queue is saturated.
  Avoids pinning the handler until NC's outbound timeout fires; the
  503 retry contract is the same as the existing "sync not running"
  branch.
- webhook_receiver: revise the compare_digest comment to match what
  the function actually guarantees — it avoids the per-character
  short-circuit of `==` but is not fully constant-time across length
  differences.
- _get_webhook_uri: read WEBHOOK_INTERNAL_URL and
  NEXTCLOUD_MCP_SERVER_URL via dynaconf so operators using
  settings.toml (rather than env vars) aren't silently routed into
  the docker/localhost fallback. Adds webhook_internal_url to
  Settings/_DEFAULTS/_field_map; nextcloud_mcp_server_url already
  existed. Docker-detection markers stay on os.getenv since they're
  container-runtime signals, not user-facing config.
- webhook_routes: sweep remaining f-string logger calls to lazy %s
  formatting per CLAUDE.md.
- client/webhooks: modernise full file's type hints to
  dict / list / | None per CLAUDE.md.

Tests:
- New test_returns_503_when_queue_is_full exercises the timeout
  branch with a saturated buffer and a shortened deadline.
- test_webhook_uri tests now patch get_settings (matching the
  auth-pair tests in the same file) instead of monkeypatching env
  vars directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-30 04:17:00 +02:00
co-authored by Claude Opus 4.7
parent 42675e7c20
commit c1368b9a7f
6 changed files with 122 additions and 52 deletions
+9 -9
View File
@@ -1,6 +1,6 @@
"""Client for Nextcloud Webhook Listeners API operations."""
from typing import Any, Dict, List, Optional
from typing import Any
from nextcloud_mcp_server.client.base import BaseNextcloudClient
@@ -11,15 +11,15 @@ class WebhooksClient(BaseNextcloudClient):
app_name = "webhooks"
def _get_webhook_headers(
self, additional_headers: Optional[Dict[str, str]] = None
) -> Dict[str, str]:
self, additional_headers: dict[str, str] | None = None
) -> dict[str, str]:
"""Get standard headers required for Webhook Listeners API calls."""
headers = {"OCS-APIRequest": "true", "Accept": "application/json"}
if additional_headers:
headers.update(additional_headers)
return headers
async def list_webhooks(self) -> List[Dict[str, Any]]:
async def list_webhooks(self) -> list[dict[str, Any]]:
"""List all registered webhooks for the current user.
Returns:
@@ -40,10 +40,10 @@ class WebhooksClient(BaseNextcloudClient):
uri: str,
http_method: str = "POST",
auth_method: str = "none",
headers: Optional[Dict[str, str]] = None,
headers: dict[str, str] | None = None,
auth_data: dict[str, str] | None = None,
event_filter: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
event_filter: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Register a new webhook for the specified event.
Args:
@@ -63,7 +63,7 @@ class WebhooksClient(BaseNextcloudClient):
Returns:
Webhook registration details including webhook ID
"""
data: Dict[str, Any] = {
data: dict[str, Any] = {
"httpMethod": http_method,
"uri": uri,
"event": event,
@@ -101,7 +101,7 @@ class WebhooksClient(BaseNextcloudClient):
headers=headers,
)
async def get_webhook(self, webhook_id: int) -> Dict[str, Any]:
async def get_webhook(self, webhook_id: int) -> dict[str, Any]:
"""Get details of a specific webhook registration.
Args: