{"record":{"id":"bbd9d300d3ca4f72","repo":"zed-industries/zed","slug":"no-such-invitation","errorCode":null,"errorMessage":"no such invitation","messagePattern":"no such invitation","errorType":"validation","errorClass":"Error::Internal","httpStatus":null,"severity":"error","filePath":"crates/collab/src/db/queries/channels.rs","lineNumber":394,"sourceCode":"\n            let membership_update = if accept {\n                let rows_affected = channel_member::Entity::update_many()\n                    .set(channel_member::ActiveModel {\n                        accepted: ActiveValue::Set(accept),\n                        ..Default::default()\n                    })\n                    .filter(\n                        channel_member::Column::ChannelId\n                            .eq(channel_id)\n                            .and(channel_member::Column::UserId.eq(user_id))\n                            .and(channel_member::Column::Accepted.eq(false)),\n                    )\n                    .exec(&*tx)\n                    .await?\n                    .rows_affected;\n\n                if rows_affected == 0 {\n                    Err(anyhow!(\"no such invitation\"))?;\n                }\n\n                Some(\n                    self.calculate_membership_updated(&channel, user_id, &tx)\n                        .await?,\n                )\n            } else {\n                let rows_affected = channel_member::Entity::delete_many()\n                    .filter(\n                        channel_member::Column::ChannelId\n                            .eq(channel_id)\n                            .and(channel_member::Column::UserId.eq(user_id))\n                            .and(channel_member::Column::Accepted.eq(false)),\n                    )\n                    .exec(&*tx)\n                    .await?\n                    .rows_affected;\n                if rows_affected == 0 {","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/zed-industries/zed/blob/bc538def4545534201bbfcac4e95ac34ea6501b6/crates/collab/src/db/queries/channels.rs#L376-L412","documentation":"Thrown in the ACCEPT branch of respond_to_channel_invite (crates/collab/src/db/queries/channels.rs:394): the code deletes the channel_member row matching (channel_id, user_id, accepted=false) and 0 rows were affected. Accepting an invite consumes the pending row, so 0 means there was no pending invitation for that user in that channel.","triggerScenarios":"Calling the invite-respond RPC with accept=true when the invite was already accepted (row now accepted=true), already declined (row deleted), revoked by an admin, or never existed. Two clients racing to accept the same invite (notification opened in two windows) — the loser gets this error.","commonSituations":"Stale notification UI letting the user click Accept on an already-handled invite; double-click/duplicate RPC from the client; retry logic re-sending an accepted respond request.","solutions":["Debounce/disable the accept button once the RPC is in flight, and mark the notification handled after the first success","Before responding, fetch the user's pending invite (pending_invite_for_channel at channels.rs:784+) and skip if None","On the server handler, consider mapping this error to an idempotent no-op when the member row already exists as accepted"],"exampleFix":"// before\ndb.respond_to_channel_invite(user_id, channel_id, true).await?;\n\n// after\nmatch db.pending_invite_for_channel(&channel, user_id, &tx).await? {\n    Some(_) => { db.respond_to_channel_invite(user_id, channel_id, true).await?; }\n    None => log::info!(\"invite already handled for channel {channel_id}\"),\n}","handlingStrategy":"validation","validationCode":"// Only respond when a pending row exists for this user+channel\nlet pending = db\n    .pending_invite_for_channel(&channel, user_id, &tx)\n    .await?;\nif pending.is_some() {\n    db.respond_to_channel_invite(user_id, channel_id, true).await?;\n}","typeGuard":null,"tryCatchPattern":"// Racing accepts: loser treats already-handled as success\nmatch db.respond_to_channel_invite(user_id, channel_id, true).await {\n    Ok(result) => Some(result),\n    Err(err) if err.to_string().contains(\"no such invitation\") => None,\n    Err(err) => return Err(err),\n}","preventionTips":["Mark invite notifications handled on first response and never re-accept","Debounce duplicate clicks on accept/decline buttons","Refresh the invite list from server state, not cache, before rendering actions"],"tags":["collab","channels","invitations","race-condition"],"backgroundTag":null,"analyzedSha":"bc538def4545534201bbfcac4e95ac34ea6501b6","analyzedAt":"2026-08-16T07:30:46.435Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}