{"record":{"id":"86b47c8b5ed6e959","repo":"paperclipai/paperclip","slug":"teams-file-destination-is-missing-its-verified-rou","errorCode":null,"errorMessage":"Teams file destination is missing its verified route","messagePattern":"Teams file destination is missing its verified route","errorType":"validation","errorClass":"TeamsServiceUrlValidationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-sdk-runtime.ts","lineNumber":1207,"sourceCode":"    threadId: string,\n    operation: () => Promise<T>,\n    requireRoute = false,\n  ): Promise<T> => {\n    const decoded = teams.decodeThreadId!(threadId);\n    if (typeof decoded.conversationId !== \"string\" || !decoded.conversationId) {\n      throw new TeamsServiceUrlValidationError(\n        \"Teams destination is missing its conversation identity\",\n      );\n    }\n    const persistedServiceUrl = await teams.chat\n      ?.getState()\n      .get(teamsConversationRouteStateKey(decoded.conversationId));\n    if (\n      requireRoute &&\n      persistedServiceUrl == null &&\n      decoded.serviceUrl == null\n    ) {\n      throw new TeamsServiceUrlValidationError(\n        \"Teams file destination is missing its verified route\",\n      );\n    }\n    return await withServiceUrl(\n      persistedServiceUrl ?? decoded.serviceUrl ?? defaultApi.serviceUrl,\n      operation,\n    );\n  };\n\n  if (enableFileConsent) {\n    if (\n      typeof teams.app.send !== \"function\" ||\n      typeof teams.app.on !== \"function\"\n    ) {\n      throw new TeamsAdapterCompatibilityError(\n        \"file-consent App hooks are unavailable\",\n      );\n    }","sourceCodeStart":1189,"sourceCodeEnd":1225,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-sdk-runtime.ts#L1189-L1225","documentation":"When requireRoute is true, withThreadServiceUrl demands a verified route before any outbound file operation: either a persisted service URL in chat state for the conversation, or a serviceUrl embedded in the decoded thread id. Microsoft requires replies to go to the authenticated serviceUrl, so sending a file card without a verified route is refused with TeamsServiceUrlValidationError.","triggerScenarios":"Sending a file-consent/file_info card via paperclipSendFileCard for a conversation where teams.chat state has no teamsConversationRouteStateKey entry and decoded.serviceUrl is null — typically before any inbound activity recorded the route, or after the persisted route expired (TTL).","commonSituations":"Proactively messaging a conversation the bot never received an activity from; state TTL expired between inbound message and file send; state store cleared or running against a fresh database while replaying old thread ids.","solutions":["Ensure an inbound activity from the conversation was accepted first, so paperclipRecordAcceptedActivity persisted the serviceUrl.","Increase the accepted-activity cache TTL or re-persist the route if it expires before file sends.","Verify the durable state store (teams.chat.getState()) is the same one used when recording the route — a swapped store loses the key.","For legacy thread ids with embedded serviceUrl, confirm decodeThreadId still returns serviceUrl; otherwise migrate ids.","Check that trustedTeamsServiceUrl is not rejecting the persisted URL earlier and leaving the route unset."],"exampleFix":"// before\nawait teams.paperclipSendFileCard(threadId, 'consent', input); // no route yet\n// after\nconst decoded = teams.decodeThreadId(threadId);\nif (!decoded.serviceUrl && !(await state.get(teamsConversationRouteStateKey(decoded.conversationId)))) {\n  throw new Error('route not verified; wait for inbound activity');\n}\nawait teams.paperclipSendFileCard(threadId, 'consent', input);","handlingStrategy":"validation","validationCode":"const decoded = teams.decodeThreadId(threadId);\nconst route = await teams.chat.getState().get(teamsConversationRouteStateKey(decoded.conversationId));\nif (route == null && decoded.serviceUrl == null) {\n  throw new Error('No verified Teams route for this conversation; wait for an inbound activity first');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await teams.paperclipSendFileCard(threadId, kind, input);\n} catch (err) {\n  if (err instanceof TeamsServiceUrlValidationError && err.message.includes('verified route')) {\n    logger.warn(`No verified route for ${threadId}; deferring file send`);\n    await deferFileSend(threadId, kind, input); // queue until an inbound activity records the route\n  }\n}","preventionTips":["Only send proactive file cards to conversations that have delivered at least one accepted inbound activity.","Persist the route durably (not just TTL cache) if file sends can happen long after the last inbound message.","Use one state store consistently between recordAcceptedActivity and withThreadServiceUrl.","Alert on route-expiry: log when a required-route lookup misses so the gap is visible."],"tags":["teams","service-url","routing","validation"],"backgroundTag":"missing-required-config-field","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}