{"record":{"id":"38a1494bc40de304","repo":"instructure/canvas-lms","slug":"unauthorized-subscribe-to-discussion-topic","errorCode":null,"errorMessage":"unauthorized","messagePattern":"unauthorized","errorType":"exception","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/subscribe_to_discussion_topic.rb","lineNumber":31,"sourceCode":"# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR\n# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more\n# details.\n#\n# You should have received a copy of the GNU Affero General Public License along\n# with this program. If not, see <http://www.gnu.org/licenses/>.\n#\n\nclass Mutations::SubscribeToDiscussionTopic < Mutations::BaseMutation\n  graphql_name \"SubscribeToDiscussionTopic\"\n\n  argument :discussion_topic_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func(\"DiscussionTopic\")\n  argument :subscribed, Boolean, required: true\n\n  field :discussion_topic, Types::DiscussionType, null: false\n  def resolve(input:)\n    discussion_topic = DiscussionTopic.find(input[:discussion_topic_id])\n    raise ActiveRecord::RecordNotFound unless discussion_topic.grants_right?(current_user, session, :read)\n    raise GraphQL::ExecutionError, \"unauthorized\" if input[:subscribed] && !discussion_topic.is_announcement && discussion_topic.subscription_hold(current_user, session)\n\n    discussion_topic.change_subscribed_state(input[:subscribed], current_user)\n\n    {\n      discussion_topic:\n    }\n  rescue ActiveRecord::RecordNotFound\n    raise GraphQL::ExecutionError, \"not found\"\n  end\nend\n","sourceCodeStart":13,"sourceCodeEnd":42,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/subscribe_to_discussion_topic.rb#L13-L42","documentation":"SubscribeToDiscussionTopic raises GraphQL::ExecutionError \"unauthorized\" when attempting to subscribe (input[:subscribed] == true) to a non-announcement topic whose subscription_hold(current_user, session) returns a truthy reason (app/graphql/mutations/subscribe_to_discussion_topic.rb:31). subscription_hold encodes business rules — e.g. topic locked, user not allowed to reply/participate — that forbid subscribing.","triggerScenarios":"Calling the mutation with subscribed: true on a regular (non-announcement) discussion where subscription_hold returns a hold message, e.g. the topic is locked/closed for comments or the user lacks participation rights.","commonSituations":"Subscribing to a topic whose comments are closed; users in read-only roles trying to subscribe; topics inside locked modules; attempting to subscribe after the availability window ended.","solutions":["Only send subscribed: true for topics that are open for participation (subscription_hold is null).","Check subscription_hold state client-side before calling the mutation.","Handle \"unauthorized\" by showing the topic's hold/reason message in the UI.","If subscribing to an announcement, note announcements are exempt but generally manage their own subscription semantics."],"exampleFix":"// before\nawait subscribe({ discussionTopicId, subscribed: true })\n// after\nif (!topic.subscriptionHold) {\n  await subscribe({ discussionTopicId, subscribed: true })\n}","handlingStrategy":"validation","validationCode":"if (subscribed && !topic.isAnnouncement && topic.subscriptionHold) throw new SkipMutation(`subscription held: ${topic.subscriptionHold}`)","typeGuard":null,"tryCatchPattern":"try { await mutate(...) } catch (e) { if (e.message === 'unauthorized') showSubscriptionHoldReason(); else throw e; }","preventionTips":["Check subscriptionHold before enabling subscribe buttons.","Disable subscribing on locked or closed topics.","Distinguish announcements (exempt from the hold check) from regular topics."],"tags":["graphql","authorization","subscription-hold","discussions","canvas"],"backgroundTag":"permission-denied","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"}