Skip to content

Latest commit

 

History

History
532 lines (442 loc) · 16.6 KB

File metadata and controls

532 lines (442 loc) · 16.6 KB

Nextcloud Webhook Testing Findings

Date: 2025-11-11 Purpose: Manual validation of Nextcloud webhook schemas and behavior for vector sync integration (ADR-010)

Executive Summary

Successfully tested and validated Nextcloud webhook payloads for file/note events and calendar events. 5 out of 6 webhook types were captured and validated against expected schemas from ADR-010 and Nextcloud documentation. One calendar deletion webhook did not fire during testing (potential Nextcloud issue or configuration).

Test Environment

  • Nextcloud Version: 30+ (Docker compose setup)
  • Webhook App: webhook_listeners (bundled, enabled)
  • MCP Server: Test endpoint at http://mcp:8000/webhooks/nextcloud
  • Background Worker: Running with 60s timeout
  • Authentication: None (test environment)

Webhooks Registered

ID Event Class Status
1 OCP\Files\Events\Node\NodeCreatedEvent ✓ Tested
2 OCP\Files\Events\Node\NodeWrittenEvent ✓ Tested
3 OCP\Files\Events\Node\NodeDeletedEvent ✓ Tested
4 OCP\Calendar\Events\CalendarObjectCreatedEvent ✓ Tested
5 OCP\Calendar\Events\CalendarObjectUpdatedEvent ✓ Tested
6 OCP\Calendar\Events\CalendarObjectDeletedEvent ✗ Not received

Captured Webhook Payloads

1. NodeCreatedEvent (File/Note Creation)

Test Action: Created note via Notes API Trigger Time: 2025-11-11 08:37:25 Webhooks Fired: 3 events (folder creation + file creation + file written)

Payload:

{
  "user": {
    "uid": "admin",
    "displayName": "admin"
  },
  "time": 1762850245,
  "event": {
    "class": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
    "node": {
      "id": 437,
      "path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
    }
  }
}

Validation:

  • ✅ Schema matches ADR-010 specification
  • ✅ Contains user object with uid and displayName
  • ✅ Contains time (Unix timestamp)
  • ✅ Contains event.class (fully qualified event name)
  • ✅ Contains event.node.id (file ID)
  • ✅ Contains event.node.path (absolute path)

Observations:

  • Creating a note via Notes API triggers 3 webhook events:
    1. NodeCreatedEvent for the parent folder (if new)
    2. NodeWrittenEvent for the parent folder
    3. NodeCreatedEvent for the actual file
    4. NodeWrittenEvent for the file (sometimes fired 2x)

2. NodeWrittenEvent (File/Note Update)

Test Action: Updated note content via Notes API Trigger Time: 2025-11-11 08:49:20

Payload:

{
  "user": {
    "uid": "admin",
    "displayName": "admin"
  },
  "time": 1762850960,
  "event": {
    "class": "OCP\\Files\\Events\\Node\\NodeWrittenEvent",
    "node": {
      "id": 437,
      "path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
    }
  }
}

Validation:

  • ✅ Schema identical to NodeCreatedEvent except for event.class
  • ✅ Same file ID (437) as creation event
  • ✅ Updated timestamp reflects actual modification time

Observations:

  • File updates trigger a single NodeWrittenEvent
  • No duplicate events fired for update operations

3. NodeDeletedEvent (File/Note Deletion)

Test Action: Deleted note via Notes API Trigger Time: 2025-11-11 08:51:34 Webhooks Fired: 2 events (file + folder deletion)

Payload:

{
  "user": {
    "uid": "admin",
    "displayName": "admin"
  },
  "time": 1762851093,
  "event": {
    "class": "OCP\\Files\\Events\\Node\\NodeDeletedEvent",
    "node": {
      "path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
    }
  }
}

Validation:

  • ✅ Schema matches ADR-010 specification
  • ⚠️ IMPORTANT: No node.id field in deletion events (only path)
  • ✅ Folder deletion triggered after file deletion (empty folder cleanup)

Observations:

  • Critical Difference: Deletion events do NOT include node.id, only node.path
  • This differs from Create/Write events which include both id and path
  • ADR-010 implementation must handle missing id field for deletions
  • Deleting a file also triggers deletion of empty parent folders

4. CalendarObjectCreatedEvent (Calendar Event Creation)

Test Action: Created calendar event via CalDAV PUT Trigger Time: 2025-11-11 08:52:50

Payload (partial - calendarData omitted for brevity):

{
  "user": {
    "uid": "admin",
    "displayName": "admin"
  },
  "time": 1762851169,
  "event": {
    "calendarId": 1,
    "class": "OCP\\Calendar\\Events\\CalendarObjectCreatedEvent",
    "calendarData": {
      "id": 1,
      "uri": "personal",
      "{http://calendarserver.org/ns/}getctag": "...",
      "{http://sabredav.org/ns}sync-token": 21,
      "{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set": [],
      "{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": [],
      "{urn:ietf:params:xml:ns:caldav}calendar-timezone": null
    },
    "objectData": {
      "id": 3,
      "uri": "webhook-test-event-001.ics",
      "lastmodified": 1762851169,
      "etag": "\"2b937b7d77dc83c77329dfdb210ba9d0\"",
      "calendarid": 1,
      "size": 297,
      "component": "vevent",
      "classification": 0,
      "uid": "webhook-test-event-001@nextcloud",
      "calendardata": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n...",
      "{http://nextcloud.com/ns}deleted-at": null
    },
    "shares": []
  }
}

Validation:

  • ✅ Schema matches Nextcloud documentation
  • ✅ Contains complete calendar metadata (calendarData)
  • ✅ Contains complete event data (objectData)
  • ✅ Includes full iCal data in objectData.calendardata
  • ✅ Includes objectData.id for database lookups
  • ⚠️ Complex: Much more metadata than file events

Observations:

  • Calendar webhooks include significantly more data than file webhooks
  • Full iCal content is embedded in objectData.calendardata
  • Event ID is in objectData.id (NOT event.id)
  • calendarData contains calendar-level metadata
  • shares array contains sharing information (empty in this test)

5. CalendarObjectUpdatedEvent (Calendar Event Update)

Test Action: Updated calendar event via CalDAV PUT Trigger Time: 2025-11-11 08:53:28

Payload (partial):

{
  "user": {
    "uid": "admin",
    "displayName": "admin"
  },
  "time": 1762851207,
  "event": {
    "calendarId": 1,
    "class": "OCP\\Calendar\\Events\\CalendarObjectUpdatedEvent",
    "calendarData": { /* same structure as creation */ },
    "objectData": {
      "id": 3,
      "uri": "webhook-test-event-001.ics",
      "lastmodified": 1762851207,
      "etag": "\"2695a18013e0991e4212b07b61d5e1e2\"",
      "calendarid": 1,
      "size": 315,
      "component": "vevent",
      "classification": 0,
      "uid": "webhook-test-event-001@nextcloud",
      "calendardata": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n...",
      "{http://nextcloud.com/ns}deleted-at": null
    },
    "shares": []
  }
}

Validation:

  • ✅ Schema identical to CalendarObjectCreatedEvent except event.class
  • ✅ Same event ID (3) as creation
  • ✅ Updated lastmodified timestamp
  • ✅ Different etag (changed from creation)
  • ✅ Larger size (315 vs 297 bytes)

Observations:

  • Update events contain full new state (not delta)
  • ETag changes on updates (useful for conflict detection)
  • Size field reflects actual iCal size

6. CalendarObjectDeletedEvent (Calendar Event Deletion)

Test Action: Deleted calendar event via CalDAV DELETE Trigger Time: 2025-11-11 08:54:47 Status:WEBHOOK DID NOT FIRE

Expected Payload (from Nextcloud docs):

{
  "user": {
    "uid": "admin",
    "displayName": "admin"
  },
  "time": <timestamp>,
  "event": {
    "calendarId": 1,
    "class": "OCP\\Calendar\\Events\\CalendarObjectDeletedEvent",
    "calendarData": { /* calendar metadata */ },
    "objectData": {
      "id": 3,
      "uri": "webhook-test-event-001.ics",
      /* ... other fields ... */
    },
    "shares": []
  }
}

Issue:

  • Calendar event was successfully deleted (verified via CalDAV PROPFIND)
  • Webhook registration confirmed (ID #6 in webhook_listeners:list)
  • Background worker running and processing other events
  • No webhook notification received after 2+ minutes

Possible Causes:

  1. Known Nextcloud bug with calendar deletion webhooks
  2. CalDAV DELETE may not trigger event system properly
  3. Deletion event may require trash bin enabled
  4. Background job may have silently failed

Recommended Actions:

  • File Nextcloud issue report
  • Test with trash bin enabled (CalendarObjectMovedToTrashEvent)
  • Check Nextcloud error logs for webhook failures
  • Verify with Nextcloud 31+ if issue persists

Schema Comparison: Expected vs Actual

File Events

Field Expected (ADR-010) Actual Match
user.uid string string
user.displayName string string
time int int
event.class string string
event.node.id string int ⚠️ Type mismatch
event.node.path string string

Type Discrepancy: node.id is documented as string but returns as int (437 instead of "437")

Calendar Events

Field Expected (Nextcloud docs) Actual Match
user.uid string string
user.displayName string string
time int int
event.class string string
event.calendarId int int
event.calendarData.* object object
event.objectData.id int int
event.objectData.uri string string
event.objectData.calendardata string string
event.objectData.lastmodified int int
event.objectData.etag string string
event.objectData.component string|null string
event.shares array array

All calendar event fields match expected schemas.

Key Findings for ADR-010 Implementation

1. Deletion Events Have Different Schema

  • File Deletions: No node.id field, only node.path
  • Calendar Deletions: Not tested (webhook didn't fire)
  • Impact: Webhook handler must check for node.id existence before using it

2. Multiple Webhooks Per Operation

  • Creating a note triggers 3-5 webhook events
  • Deleting a note triggers 2 events (file + folder)
  • Impact: Deduplication logic needed in webhook handler

3. Event-Specific ID Fields

  • File events: event.node.id
  • Calendar events: event.objectData.id
  • Impact: Event parser must handle different ID field locations

4. Full State vs Delta

  • All webhooks contain complete current state (not delta)
  • Impact: No need for "previous state" tracking in webhook handler

5. Calendar Data Richness

  • Calendar webhooks include full iCal content
  • Impact: Can extract all event metadata without additional API calls

Recommendations for ADR-010 Implementation

1. Webhook Event Parser (webhook_parser.py)

def extract_document_task(event_class: str, payload: dict) -> DocumentTask | None:
    """Extract DocumentTask from webhook event payload."""
    user_id = payload["user"]["uid"]
    event_data = payload["event"]

    # File/Note events
    if "NodeCreatedEvent" in event_class or "NodeWrittenEvent" in event_class:
        path = event_data["node"]["path"]

        # Only process markdown files for notes
        if not path.endswith(".md"):
            return None

        # IMPORTANT: Check if 'id' exists (missing in deletion events)
        doc_id = str(event_data["node"].get("id", ""))
        if not doc_id:
            # For missing ID, use path-based identifier
            doc_id = f"path:{path}"

        return DocumentTask(
            user_id=user_id,
            doc_id=doc_id,
            doc_type="note",
            operation="index",
            modified_at=payload["time"],
        )

    # File deletion events
    elif "NodeDeletedEvent" in event_class:
        path = event_data["node"]["path"]

        if not path.endswith(".md"):
            return None

        # Deletion events DON'T have node.id - use path
        return DocumentTask(
            user_id=user_id,
            doc_id=f"path:{path}",  # Path-based since ID unavailable
            doc_type="note",
            operation="delete",
            modified_at=payload["time"],
        )

    # Calendar creation/update events
    elif "CalendarObjectCreatedEvent" in event_class or \
         "CalendarObjectUpdatedEvent" in event_class:
        return DocumentTask(
            user_id=user_id,
            doc_id=str(event_data["objectData"]["id"]),
            doc_type="calendar_event",
            operation="index",
            modified_at=event_data["objectData"]["lastmodified"],
        )

    # Calendar deletion events
    elif "CalendarObjectDeletedEvent" in event_class:
        return DocumentTask(
            user_id=user_id,
            doc_id=str(event_data["objectData"]["id"]),
            doc_type="calendar_event",
            operation="delete",
            modified_at=payload["time"],
        )

    return None  # Unsupported event type

2. Deduplication Strategy

Problem: Creating a note triggers 3-5 webhooks Solution: Idempotent processing + task deduplication

# In webhook handler
async def handle_nextcloud_webhook(request: Request) -> JSONResponse:
    payload = await request.json()

    task = extract_document_task(
        payload["event"]["class"],
        payload
    )

    if task:
        # Idempotent: Queue will only process latest version
        await document_queue.send(task)

    return JSONResponse({"status": "received"}, status_code=200)

3. Path-Based Fallback for Deletions

Since deletion events lack node.id, use path-based identification:

# In Qdrant delete logic
async def delete_document(user_id: str, doc_id: str, doc_type: str):
    if doc_id.startswith("path:"):
        # Path-based deletion
        path = doc_id.removeprefix("path:")
        # Search Qdrant for document with matching path in metadata
        points = await qdrant.scroll(
            collection_name=collection,
            scroll_filter=Filter(must=[
                FieldCondition(
                    key="user_id",
                    match=MatchValue(value=user_id),
                ),
                FieldCondition(
                    key="metadata.path",
                    match=MatchValue(value=path),
                ),
            ]),
        )
        # Delete found points
    else:
        # ID-based deletion (normal case)
        ...

4. Webhook Registration Filters

To reduce webhook volume, add filters:

{
  "httpMethod": "POST",
  "uri": "http://mcp:8000/webhooks/nextcloud",
  "event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
  "eventFilter": {
    "event.node.path": "/^.*\\.md$/"
  }
}

This filters to only .md files at the webhook registration level (not handler level).

5. Monitoring and Metrics

Add webhook-specific metrics:

webhook_notifications_received_total{event_type="note_created"} 42
webhook_processing_duration_seconds{event_type="note_created"} 0.023
webhook_errors_total{error_type="parse_error"} 2
webhook_duplicates_filtered_total{doc_type="note"} 15

Testing Checklist for Implementation

  • File creation webhook triggers document indexing
  • File update webhook triggers reindexing
  • File deletion webhook triggers document removal
  • File deletion without ID successfully removes document (path-based)
  • Calendar creation webhook triggers event indexing
  • Calendar update webhook triggers event reindexing
  • Calendar deletion webhook triggers event removal (NOT TESTED - webhook didn't fire)
  • Duplicate webhooks are deduplicated
  • Non-markdown file webhooks are ignored
  • Malformed webhook payloads return 400 error
  • Webhook authentication validates shared secret
  • Webhook processing completes within 50ms

Appendix: Raw Webhook Logs

Complete webhook logs with full payloads are available in MCP container logs:

docker compose logs mcp | grep -A 30 "🔔 Webhook received"

Conclusion

Nextcloud webhooks work as documented with minor exceptions:

  1. File/Note Events: Fully functional and match expected schemas
  2. Calendar Creation/Update: Fully functional with rich metadata
  3. Calendar Deletion: Webhook did not fire (requires investigation)
  4. ⚠️ Schema Discrepancy: node.id is integer (not string as documented)
  5. ⚠️ Deletion Schema: Missing node.id field (only path provided)

Overall Status: Ready for ADR-010 implementation with noted caveats. Calendar deletion webhook issue should be reported to Nextcloud and may require alternative approach (polling or trash bin events).