koala73/worldmonitor · error · ConvexError

webhookEnvelope required for webhook channel

Error message

webhookEnvelope required for webhook channel

What it means

Thrown by the `setChannel` Convex mutation when `args.channelType === "webhook"` but `args.webhookEnvelope` is empty or undefined. The webhook channel document requires a `webhookEnvelope` payload (the URL/secret envelope), so the call is rejected before insertion. Plain string-typed ConvexError; client reads `err.data` as the message.

Source

Thrown at convex/notificationChannels.ts:517

      }
    } else if (args.channelType === "slack") {
      if (!args.webhookEnvelope) throw new ConvexError("webhookEnvelope required for slack channel");
      const doc = { userId, channelType: "slack" as const, webhookEnvelope: args.webhookEnvelope, verified: true, linkedAt: now };
      if (existing) {
        await ctx.db.replace(existing._id, doc);
      } else {
        await ctx.db.insert("notificationChannels", doc);
      }
    } else if (args.channelType === "email") {
      if (!args.email) throw new ConvexError("email required for email channel");
      const doc = { userId, channelType: "email" as const, email: args.email, verified: true, linkedAt: now };
      if (existing) {
        await ctx.db.replace(existing._id, doc);
      } else {
        await ctx.db.insert("notificationChannels", doc);
      }
    } else if (args.channelType === "webhook") {
      if (!args.webhookEnvelope) throw new ConvexError("webhookEnvelope required for webhook channel");
      const doc = { userId, channelType: "webhook" as const, webhookEnvelope: args.webhookEnvelope, verified: true, linkedAt: now, webhookLabel: args.webhookLabel };
      if (existing) {
        await ctx.db.replace(existing._id, doc);
      } else {
        await ctx.db.insert("notificationChannels", doc);
      }
    } else {
      throw new ConvexError("discord channel must be set via set-discord-oauth");
    }
  },
});

export const deleteChannel = mutation({
  args: { channelType: channelTypeValidator },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError("UNAUTHENTICATED");
    const userId = identity.subject;

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Pass a non-empty `webhookEnvelope` string: `setChannel({ channelType: "webhook", webhookEnvelope, webhookLabel })`.
  2. Gate the submit on a non-empty webhook URL before constructing the envelope.
  3. Confirm the envelope is serialized to the exact string shape the server expects (URL + secret), not left as a JS object.

Example fix

// before
await setChannel({ channelType: "webhook", webhookLabel: "CI" });

// after
await setChannel({ channelType: "webhook", webhookEnvelope: JSON.stringify({ url, secret }), webhookLabel: "CI" });
Defensive patterns

Strategy: validation

Validate before calling

if (channelType === "webhook" && !webhookEnvelope) {
  setFieldError("url", "Webhook URL/envelope is required");
  return;
}
await setChannel({ channelType: "webhook", webhookEnvelope, webhookLabel });

Type guard

function hasWebhookEnvelope(a: { webhookEnvelope?: string }): a is { webhookEnvelope: string } {
  return typeof a.webhookEnvelope === "string" && a.webhookEnvelope.length > 0;
}

Prevention

When it happens

Trigger: Calling `setChannel({ channelType: "webhook" })` without a `webhookEnvelope`. Building args from a partial form state where the webhook URL/secret envelope was never populated.

Common situations: A custom-webhook settings UI submitted with an empty URL field; the envelope object failed to serialize before the call; the caller passed `webhookLabel` only and forgot the envelope.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/c0afd636c06c7819. Report an issue: GitHub.