Skip to content

[FEAT] : Webhook Redelivery Nested Payload Bug  #3128

Description

@hrshjswniii

Problem Statement

Outbound webhooks in DevTrack dispatch a structured payload of type WebhookPayload (matching the shape { event: string, timestamp: string, data: Record<string, unknown> }). When a webhook delivery fails, this complete payload structure is logged in the webhook_deliveries table under the payload column.

When a developer clicks "Retry Delivery" in the webhooks dashboard, the retry route handler /api/webhooks/custom/[id]/deliveries/[deliveryId]/retry/route.ts reads this logged payload from the database and forwards it directly as the third parameter (data) to dispatchWebhook. Because dispatchWebhook automatically wraps its data argument in a new WebhookPayload shell, the final HTTP POST request is sent with a corrupt, nested structure:

{
  "event": "goal.completed",
  "timestamp": "2026-07-04T10:00:00.000Z",
  "data": {
    "event": "goal.completed",
    "timestamp": "2026-07-03T12:00:00.000Z",
    "data": {
      // Original data is nested here
    }
  }
}

This violates the API contract and breaks integrations that parse the webhook payload structure.

tch.

Proposed Solution

Extract and forward the nested data block of the original payload during redelivery:

  1. In the retry route handler src/app/api/webhooks/custom/[id]/deliveries/[deliveryId]/retry/route.ts, parse the delivery.payload database column.
  2. Check if the parsed payload follows the standard WebhookPayload structure (i.e. contains a top-level data key).
  3. If it does, extract payloadData.data and pass that extracted dictionary to dispatchWebhook instead of the full root object:
    // Extract original inner data to prevent double-nesting on dispatch
    const originalData = (payloadData && typeof payloadData === "object" && "data" in payloadData)
      ? (payloadData.data as Record<string, unknown>)
      : payloadData;
    
    const result2 = await dispatchWebhook(id, delivery.event, originalData);
  4. This ensures that when dispatchWebhook constructs the new payload, it wraps only the original event fields.

Feature Area

API / Backend

Alternatives Considered

  • Modifying dispatchWebhook: We considered modifying the core helper in src/lib/webhooks.ts to detect if the incoming data is already wrapped in a WebhookPayload. However, dispatchWebhook is called from various live events (goals, streaks, achievements) that always supply raw dictionaries. Adding check-logic there introduces unnecessary complexity to a critical path. Normalizing the input at the retry route boundary is cleaner and safer.

Acceptance Criteria

  • Outbound webhook retries dispatch a payload structure that matches the original dispatch payload format (no nested event/timestamp keys inside the data object).
  • Unit tests exist covering retry payload formatting, ensuring that the payload remains properly un-nested during a simulated retry dispatch.

Additional Context

No response

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestgssoc:assignedGSSoC: Issue assigned to a contributorneeds-triageNeeds maintainer triage

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions