Messages stuck in "sending..." forever when forum topic creation fails
Description
The error handler for _internal_createForumChannelTopic() is empty in submodules/TelegramCore/Sources/State/PendingMessageManager.swift:598:
}, error: { _ in
//TODO:release handle errors
}))
If topic creation fails due to permissions, a topic limit, or a network error, all messages targeting that topic remain stuck in .waitingForNewTopic.
They never transition to .Failed, so the user sees an infinite spinner with no way to retry.
Steps to Reproduce
- Open a forum-enabled channel where you don't have topic creation permissions (or where the topic limit is reached).
- Tap "New Topic", write a message, send it.
- The client fires
_internal_createForumChannelTopic() → server returns an error (CHAT_ADMIN_REQUIRED, CHANNELS_TOO_MUCH, etc.).
- Messages are stuck.
This is also reproducible with a transient network failure during the createForumChannelTopic
Expected vs Actual
Expected: Messages transition to the failed state. The UI shows the red error indicator, and the user can retry or delete them.
Actual: Messages stay in "sending..." indefinitely.
Impact
This causes effective data loss: the user composes a message, sends it, and it disappears into permanent "sending..." limbo.
Root Cause
Three things compound here:
1) Empty error closure
The _internal_createForumChannelTopic signal can fail, and nothing happens in response.
The success path in submodules/TelegramCore/Sources/State/PendingMessageManager.swift:537 correctly moves messages to the new topic and resumes sending.
The error path at submodules/TelegramCore/Sources/State/PendingMessageManager.swift:598 does nothing.
// submodules/TelegramCore/Sources/State/PendingMessageManager.swift:527
disposable.set(_internal_createForumChannelTopic(
postbox: strongSelf.postbox,
network: strongSelf.network,
// ...
).startStrict(next: { [weak strongSelf] topicId in
// success: moves messages, resumes pipeline — works fine
}, error: { _ in
//TODO:release handle errors // <-- bug
}))
2) newTopicDisposables not cleaned up on error
On success, the disposable is cleaned up in submodules/TelegramCore/Sources/State/PendingMessageManager.swift:587:
strongSelf.newTopicDisposables[messagePeerId]?.dispose()
strongSelf.newTopicDisposables[messagePeerId] = nil
On error, nothing cleans it up. The guard at submodules/TelegramCore/Sources/State/PendingMessageManager.swift:518 (if strongSelf.newTopicDisposables[messagePeerId] == nil) then blocks any future retry because the key is still in the dictionary.
3) beginSendingMessages can't recover them
When the system re-triggers beginSendingMessages (for example, after a network change), the messages already have a PendingMessageContext in .waitingForNewTopic. But beginSendingMessages only picks up messages in .collectingInfo at submodules/TelegramCore/Sources/State/PendingMessageManager.swift:606. As a result, these messages are orphaned and the pipeline can no longer reach them.
Proposed Fix
Handle the error path of _internal_createForumChannelTopic() symmetrically to the success path:
- Mark all pending messages for that peer as failed instead of leaving them in
.waitingForNewTopic.
- Clear
newTopicDisposables[peerId] on error so retry is possible.
- Ensure a later retry (
resendMessages, reconnect, or a manual retry) re-enters the topic-creation flow.
One possible implementation is to replace the empty error closure with logic that collects affected messages, marks them as failed via failMessages(), notifies subscribers, and clears the disposable:
}, error: { [weak strongSelf] _ in
guard let strongSelf else {
return
}
var failMessageIds: [MessageId] = []
for (_, messageContext) in strongSelf.messageContexts {
if case let .waitingForNewTopic(message) = messageContext.state {
if message.id.peerId == messagePeerId {
failMessageIds.append(message.id)
}
}
}
if !failMessageIds.isEmpty {
let _ = (failMessages(
postbox: strongSelf.postbox,
ids: failMessageIds
)
|> deliverOn(strongSelf.queue)).startStandalone(completed: { [weak strongSelf] in
guard let strongSelf else {
return
}
for id in failMessageIds {
if let context = strongSelf.messageContexts[id] {
context.error = .flood
context.state = .none
for subscriber in context.statusSubscribers.copyItems() {
subscriber(nil, context.error)
}
}
}
if let summaryContext = strongSelf.peerSummaryContexts[messagePeerId] {
for subscriber in summaryContext.messageFailedSubscribers.copyItems() {
subscriber(.flood)
}
}
})
}
strongSelf.newTopicDisposables[messagePeerId]?.dispose()
strongSelf.newTopicDisposables[messagePeerId] = nil
}))
Ideally, a dedicated PendingMessageFailureReason.topicCreationFailed case would be better
Acceptance Criteria
- On topic creation error, affected messages transition to
.Failed instead of remaining in .waitingForNewTopic.
- After a failure, retry or
resendMessages attempts topic creation again.
newTopicDisposables[peerId] is cleared on error.
Testing
- Mock
_internal_createForumChannelTopic to return an error. Enqueue messages with threadId == Message.newTopicThreadId.
- After failure, call
resendMessages. Assert messages re-enter the pipeline and topic creation is attempted again.
- After error, assert
newTopicDisposables[peerId] is nil.
- Enqueue 3+ messages for the same topic, trigger an error, and assert that all of them fail, not just the first.
- Manual: forum channel, no permissions to create topics. Send message → should show red error indicator, retry/delete should work.
Messages stuck in "sending..." forever when forum topic creation fails
Description
The error handler for
_internal_createForumChannelTopic()is empty insubmodules/TelegramCore/Sources/State/PendingMessageManager.swift:598:If topic creation fails due to permissions, a topic limit, or a network error, all messages targeting that topic remain stuck in
.waitingForNewTopic.They never transition to
.Failed, so the user sees an infinite spinner with no way to retry.Steps to Reproduce
_internal_createForumChannelTopic()→ server returns an error (CHAT_ADMIN_REQUIRED,CHANNELS_TOO_MUCH, etc.).This is also reproducible with a transient network failure during the
createForumChannelTopicExpected vs Actual
Expected: Messages transition to the failed state. The UI shows the red error indicator, and the user can retry or delete them.
Actual: Messages stay in "sending..." indefinitely.
Impact
This causes effective data loss: the user composes a message, sends it, and it disappears into permanent "sending..." limbo.
Root Cause
Three things compound here:
1) Empty error closure
The
_internal_createForumChannelTopicsignal can fail, and nothing happens in response.The success path in
submodules/TelegramCore/Sources/State/PendingMessageManager.swift:537correctly moves messages to the new topic and resumes sending.The error path at
submodules/TelegramCore/Sources/State/PendingMessageManager.swift:598does nothing.2)
newTopicDisposablesnot cleaned up on errorOn success, the disposable is cleaned up in
submodules/TelegramCore/Sources/State/PendingMessageManager.swift:587:On error, nothing cleans it up. The guard at
submodules/TelegramCore/Sources/State/PendingMessageManager.swift:518(if strongSelf.newTopicDisposables[messagePeerId] == nil) then blocks any future retry because the key is still in the dictionary.3)
beginSendingMessagescan't recover themWhen the system re-triggers
beginSendingMessages(for example, after a network change), the messages already have aPendingMessageContextin.waitingForNewTopic. ButbeginSendingMessagesonly picks up messages in.collectingInfoatsubmodules/TelegramCore/Sources/State/PendingMessageManager.swift:606. As a result, these messages are orphaned and the pipeline can no longer reach them.Proposed Fix
Handle the error path of
_internal_createForumChannelTopic()symmetrically to the success path:.waitingForNewTopic.newTopicDisposables[peerId]on error so retry is possible.resendMessages, reconnect, or a manual retry) re-enters the topic-creation flow.One possible implementation is to replace the empty error closure with logic that collects affected messages, marks them as failed via
failMessages(), notifies subscribers, and clears the disposable:Ideally, a dedicated
PendingMessageFailureReason.topicCreationFailedcase would be betterAcceptance Criteria
.Failedinstead of remaining in.waitingForNewTopic.resendMessagesattempts topic creation again.newTopicDisposables[peerId]is cleared on error.Testing
_internal_createForumChannelTopicto return an error. Enqueue messages withthreadId == Message.newTopicThreadId.resendMessages. Assert messages re-enter the pipeline and topic creation is attempted again.newTopicDisposables[peerId]is nil.