paperclipai/paperclip · error · TeamsServiceUrlValidationError

Teams file cards require an exact personal conversation

Error message

Teams file cards require an exact personal conversation

What it means

Teams file cards can only be delivered inside a one-to-one personal conversation. When paperclipSendFileCard decodes the thread ID, it validates that the conversation type is 'personal' and that the conversation ID is a clean, bounded string (no control/whitespace chars, no ';messageid=' suffix). If any of these checks fail, this TeamsServiceUrlValidationError is thrown before any provider call is made.

Source

Thrown at server/src/services/chat-sdk-runtime.ts:1245

      const card =
        kind === "consent"
          ? parseTeamsFileConsentCard(input)
          : kind === "file_info"
            ? parseTeamsUploadedFileCard(input)
            : null;
      if (!card)
        throw new TeamsAdapterCompatibilityError("invalid file-card shape");
      const decoded = teams.decodeThreadId!(threadId);
      // Never infer personal scope from a missing type or conversation prefix.
      if (
        decoded.conversationType !== "personal" ||
        typeof decoded.conversationId !== "string" ||
        !decoded.conversationId ||
        decoded.conversationId.length > 1024 ||
        /[\x00-\x20\x7f]/.test(decoded.conversationId) ||
        /;messageid=/i.test(decoded.conversationId)
      ) {
        throw new TeamsServiceUrlValidationError(
          "Teams file cards require an exact personal conversation",
        );
      }
      return await withThreadServiceUrl(
        threadId,
        async () => {
          let result: unknown;
          try {
            // Direct App attachment send is deliberately inside the same route
            // ALS as ordinary thread operations. Thread.post would turn these
            // provider-native cards into AdaptiveCards.
            result = await teams.app!.send!(decoded.conversationId as string, {
              type: "message",
              attachments: [card],
            });
          } catch {
            // Provider errors may contain private card contexts. No implicit
            // retry: an uncertain POST remains caller-owned durable evidence.

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the threadId refers to a personal (1:1) Teams conversation before sending a file card; decode it with teams.decodeThreadId and check conversationType === 'personal'.
  2. Strip any ';messageid=...' suffix from the conversation ID and use the bare conversation ID.
  3. Re-obtain the thread ID from an inbound personal-chat activity rather than deriving it from a channel or group message.
  4. Sanitize the conversation ID: it must be non-empty, <=1024 chars, and contain no control characters (\x00-\x20, \x7f).

Example fix

// before
await teams.paperclipSendFileCard(channelThreadId, "consent", input);
// after
const decoded = teams.decodeThreadId(channelThreadId);
if (decoded.conversationType !== "personal") {
  throw new Error("File cards require a personal chat thread");
}
await teams.paperclipSendFileCard(channelThreadId, "consent", input);
Defensive patterns

Strategy: validation

Validate before calling

const d = teams.decodeThreadId(threadId);
const ok = d.conversationType === "personal" && typeof d.conversationId === "string" &&
  d.conversationId.length > 0 && d.conversationId.length <= 1024 &&
  !/[\x00-\x20\x7f]/.test(d.conversationId) && !/;messageid=/i.test(d.conversationId);
if (!ok) throw new Error("refusing to send file card: not a clean personal conversation");

Type guard

function isPersonalConversation(d: { conversationType?: string; conversationId?: unknown }): d is { conversationType: "personal"; conversationId: string } {
  return d.conversationType === "personal" && typeof d.conversationId === "string" &&
    d.conversationId.length > 0 && d.conversationId.length <= 1024 &&
    !/[\x00-\x20\x7f]/.test(d.conversationId) && !/;messageid=/i.test(d.conversationId);
}

Try / catch

try {
  await teams.paperclipSendFileCard(threadId, kind, input);
} catch (err) {
  if (err instanceof TeamsServiceUrlValidationError) {
    logger.warn({ threadId }, "file card skipped: thread is not a personal conversation");
    return; // skip card delivery, fall back to plain message
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling teams.paperclipSendFileCard with a threadId whose decoded conversationType is not 'personal' (e.g. channel or groupChat), whose decoded.conversationId is empty/non-string, exceeds 1024 chars, contains control or whitespace characters, or embeds a ';messageid=' segment.

Common situations: Passing a channel thread ID where a DM thread ID is expected; using a conversationId copied from a message reference (which appends ;messageid=...); storing/reconstructing thread IDs in a lossy format that injects whitespace or control bytes.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/132005fc6558f8e2. Report an issue: GitHub.