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:
co-authored by
Claude Opus 4.7
parent
42675e7c20
commit
c1368b9a7f
@@ -8,6 +8,7 @@ in :mod:`nextcloud_mcp_server.app`.
|
||||
import hmac
|
||||
import logging
|
||||
|
||||
import anyio
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
@@ -53,9 +54,10 @@ async def handle_nextcloud_webhook(request: Request) -> JSONResponse:
|
||||
if secret:
|
||||
provided = request.headers.get("authorization", "")
|
||||
expected = f"Bearer {secret}"
|
||||
# Always run compare_digest so the constant-time path is taken even
|
||||
# when the header is missing — `compare_digest("", expected)` returns
|
||||
# False without leaking length information.
|
||||
# Use compare_digest to avoid the character-by-character short-circuit
|
||||
# of `==`. compare_digest still returns False for differing lengths
|
||||
# but isn't fully constant-time across them; that's fine here — a
|
||||
# secret length leak is not a sensitive signal.
|
||||
if not hmac.compare_digest(provided, expected):
|
||||
logger.warning("Webhook rejected: missing or invalid Authorization header")
|
||||
return JSONResponse(
|
||||
@@ -94,7 +96,21 @@ async def handle_nextcloud_webhook(request: Request) -> JSONResponse:
|
||||
)
|
||||
|
||||
try:
|
||||
await send_stream.send(task)
|
||||
with anyio.fail_after(1.0):
|
||||
await send_stream.send(task)
|
||||
except TimeoutError:
|
||||
# Queue is saturated (default 10 000 tasks). Returning 503 lets NC
|
||||
# retry rather than pinning this handler until its outbound timeout
|
||||
# fires; the queue-pressure signal also surfaces in metrics.
|
||||
logger.warning(
|
||||
"Webhook task drop: queue full for %s_%s",
|
||||
task.doc_type,
|
||||
task.doc_id,
|
||||
)
|
||||
return JSONResponse(
|
||||
{"status": "unavailable", "reason": "queue full"},
|
||||
status_code=503,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to queue webhook task for %s_%s: %s",
|
||||
|
||||
Reference in New Issue
Block a user