{"record":{"id":"efaa355d8e96a7cb","repo":"instructure/canvas-lms","slug":"not-found-add-conversation-message","errorCode":null,"errorMessage":"not found","messagePattern":"not found","errorType":"exception","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/add_conversation_message.rb","lineNumber":65,"sourceCode":"      message_ids: input[:included_messages],\n      body: input[:body],\n      attachment_ids: input[:attachment_ids],\n      domain_root_account_id: context[:domain_root_account].id,\n      media_comment_id: input[:media_comment_id],\n      media_comment_type: input[:media_comment_type]\n    )\n    InstStatsd::Statsd.distributed_increment(\"inbox.message.sent.isReply.react\")\n    InstStatsd::Statsd.distributed_increment(\"inbox.message.sent.react\")\n    InstStatsd::Statsd.count(\"inbox.message.sent.recipients.react\", message[:recipients_count])\n    if input[:media_comment_id] || ConversationMessage.where(id: message[:message]&.id).first&.has_media_objects\n      InstStatsd::Statsd.distributed_increment(\"inbox.message.sent.media.react\")\n    end\n    if !message[:message].nil? && message[:message][:attachment_ids].present?\n      InstStatsd::Statsd.distributed_increment(\"inbox.message.sent.attachment.react\")\n    end\n    { conversation_message: message[:message] }\n  rescue ActiveRecord::RecordNotFound\n    raise GraphQL::ExecutionError, \"not found\"\n  rescue ActiveRecord::RecordInvalid => e\n    errors_for(e.record)\n  rescue ConversationsHelper::Error => e\n    validation_error(e.message)\n  end\n\n  def get_conversation(id)\n    conversation = current_user.all_conversations.find_by(conversation_id: id)\n    raise ActiveRecord::RecordNotFound unless conversation\n\n    conversation\n  end\nend\n","sourceCodeStart":47,"sourceCodeEnd":79,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/add_conversation_message.rb#L47-L79","documentation":"AddConversationMessage#resolve rescues ActiveRecord::RecordNotFound and raises the terse 'not found' GraphQL::ExecutionError. Inside resolve, conversation or participant lookups (e.g. Conversation.find / participant verification for conversation_id) raise RecordNotFound when the id does not exist or is not visible to the current user, so the whole mutation fails with this generic message.","triggerScenarios":"Mutation addConversationMessage called with a conversation_id that doesn't exist, was deleted from the inbox, belongs to another user, or is malformed; also a conversation record purged between listing and message send.","commonSituations":"Mobile clients caching conversation ids and sending to a conversation archived/deleted elsewhere; two devices racing (one deletes the thread, the other replies); scripts iterating stale conversation id lists; ids copied across environments.","solutions":["Refetch the conversation list and use a current conversation_id, discarding stale cached ids.","Handle the 'not found' GraphQL error client-side by refreshing the inbox and informing the user the conversation no longer exists.","Confirm the id belongs to the same environment and to the authenticated user's conversations.","Retry after refresh only if the conversation still exists; otherwise drop the draft or move it to the sender's drafts.","In code, pre-check visibility: scope lookups through the user's visible conversations instead of a bare find."],"exampleFix":"// before\nrescue ActiveRecord::RecordNotFound\n  raise GraphQL::ExecutionError, \"not found\"\n// after\nrescue ActiveRecord::RecordNotFound\n  raise GraphQL::ExecutionError, \"conversation not found or not visible to current user\"\nend\n// caller:\ntry { await addConversationMessage(id) } catch (e) { if (/not found/.test(e.message)) refreshInbox() }","handlingStrategy":"try-catch","validationCode":"// verify the conversation is still visible before sending\nconst conv = await fetchConversation(conversationId) // from the user's own conversation list\nif (!conv) throw new Error(\"conversation no longer exists; refresh inbox before replying\")","typeGuard":"const isAccessibleConversation = (c) => !!c && c.visible !== false && c.audience?.includes(currentUser.id)","tryCatchPattern":"try {\n  await client.request(ADD_MESSAGE_MUTATION, { conversationId, body })\n} catch (e) {\n  if (e.message === \"not found\") {\n    await refreshInbox()\n    return notify(\"This conversation is no longer available\")\n  }\n  throw e\n}","preventionTips":["Don't cache conversation ids long-term; re-fetch before sending","Handle concurrent deletion across devices","Scope lookups through the current user's conversations server-side","Surface drafts so a failed send isn't lost"],"tags":["graphql","mutation","conversations","record-not-found"],"backgroundTag":"record-not-found","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}