Spaces:
Runtime error
Runtime error
| import asyncio | |
| import hashlib | |
| import hmac | |
| import json | |
| import logging | |
| import httpx | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from src.config import settings | |
| from src.crud.webhook import list_webhook_endpoints | |
| from src.dependencies import tracked_db | |
| from src.utils.formatting import utc_now_iso | |
| from src.utils.queue_payload import WebhookPayload | |
| logger = logging.getLogger(__name__) | |
| async def deliver_webhook(payload: WebhookPayload, workspace_name: str) -> None: | |
| """ | |
| Deliver a single webhook event to its configured endpoints. | |
| """ | |
| try: | |
| async with tracked_db("webhook.deliver") as db: | |
| webhook_urls = await _get_webhook_urls(db, workspace_name) | |
| if not webhook_urls: | |
| logger.debug( | |
| f"No webhook endpoints for workspace {workspace_name}, skipping." | |
| ) | |
| return | |
| event_payload = { | |
| "type": payload.event_type, | |
| "data": payload.data, | |
| "timestamp": utc_now_iso(), | |
| } | |
| event_json = json.dumps(event_payload, separators=(",", ":"), sort_keys=True) | |
| try: | |
| signature = _generate_webhook_signature(event_json) | |
| except ValueError: | |
| logger.exception("Failed to generate webhook signature") | |
| return | |
| async with httpx.AsyncClient(timeout=30.0) as client: | |
| tasks = [ | |
| client.post( | |
| url=url, | |
| content=event_json, | |
| headers={ | |
| "Content-Type": "application/json", | |
| "X-Honcho-Signature": signature, | |
| }, | |
| ) | |
| for url in webhook_urls | |
| ] | |
| results = await asyncio.gather(*tasks, return_exceptions=True) | |
| for url, result in zip(webhook_urls, results, strict=False): | |
| if isinstance(result, httpx.Response): | |
| if 200 <= result.status_code < 300: | |
| logger.debug( | |
| f"Successfully delivered webhook {payload.event_type} to {url}" | |
| ) | |
| else: | |
| logger.error( | |
| f"Failed delivery for {payload.event_type} to {url}. Status: {result.status_code}" | |
| ) | |
| else: | |
| logger.error( | |
| f"Failed delivery for {payload.event_type} to {url}. Exception: {result}" | |
| ) | |
| except httpx.RequestError: | |
| logger.exception(f"Error sending webhook for {workspace_name}.") | |
| except Exception: | |
| logger.exception("Unexpected error delivering webhook.") | |
| async def _get_webhook_urls(db: AsyncSession, workspace_name: str) -> list[str]: | |
| """ | |
| Get all webhook endpoint URLs for a workspace. | |
| """ | |
| try: | |
| endpoints = await list_webhook_endpoints(workspace_name) | |
| result = await db.execute(endpoints) | |
| return [endpoint.url for endpoint in result.scalars().all()] | |
| except Exception: | |
| logger.exception(f"Error fetching endpoints for {workspace_name}") | |
| return [] | |
| def _generate_webhook_signature(payload: str) -> str: | |
| """ | |
| Generate HMAC-SHA256 signature for webhook payload using WEBHOOK_SECRET. | |
| """ | |
| webhook_secret = settings.WEBHOOK.SECRET | |
| if not webhook_secret: | |
| raise ValueError("WEBHOOK_SECRET not found - cannot sign webhook") | |
| return hmac.new( | |
| webhook_secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256 | |
| ).hexdigest() | |