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
- 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'.
- Strip any ';messageid=...' suffix from the conversation ID and use the bare conversation ID.
- Re-obtain the thread ID from an inbound personal-chat activity rather than deriving it from a channel or group message.
- 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
- Always derive threadId from activity.conversation.id of an inbound personal chat.
- Strip ';messageid=' suffixes before using conversation IDs.
- Never reuse channel/group thread IDs for file-card sends.
- Validate decoded.conversationType before any card operation.
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
- Teams destination is missing its conversation identity
- Teams file destination is missing its verified route
- invalid file-card shape
- Image exceeds attachment bound
- Invalid Teams private binding
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/132005fc6558f8e2.
Report an issue: GitHub.