App crash on creating a new 1:1 chat, found nil when unwrapping optional in ListDataBaseObserver
oleg-rocks opened this issue ยท 26 comments
What did you do?
Create a new 1:1 direct message chat with a new user with empty list of chats (may also happen with a user with some other chats in the list). Both the creator of the chat and the target user have their phones open on the chat list screen.
What did you expect to happen?
New chat appear on the chat list screen.
What happened instead?
App crash with an error "Unexpectedly found nil when implicitly unwrapping an Optional value" in ListDataBaseObserver class in StreamChat source code. The app is crashing on both creator's phone and the target user's phone if they both use iOS.
Video of the issue
https://github.com/GetStream/stream-chat-swift/assets/80982911/4b7818a5-4c05-49bd-88b5-d2901c5221cf
GetStream Environment
GetStream Chat version: 4.48.0
GetStream Chat frameworks: StreamChat, StreamChatUI
iOS version: 17.2
Swift version: 5.8.1
Xcode version: 15.2
Device: phone A (creator of the chat) iPhone 8, iOS 16.7.2, phone B (target user) iPhone 15 iOS 17.2
May be also reproduced when creating a 1:1 message chat from Android phone with an iOS user. In such case the Android user's app does not crash, but iOS user's app crashes.
Additional context
Interesting observation after an app crash and subsequent relaunch: the list of chats on the chat creator's phone does not contain the new chat. On the target user's phone, the new chat appears but without the delete option. Once there's a message inside the chat, it appears on the creator's phone, and the creator has the right to delete it.
Hi @kirsanov-dev,
Thanks for opening the issue.
Unfortunately, I was not able to reproduce it on our side. Could you clarify something, please:
- Could you share the crash log?
- Could you also share the code snippets on how you're doing the logout? In particular I'm interested if you're waiting for the completion block of the logout function.
- Could you also share how you are creating the ChatClient? Do you destroy the ChatClient at any point?
- Do you have Background Mapping enabled?
- Could you check out 4.49.0? We had some regression in 4.48.0. Even though that one is not related, it's worth trying the latest release, just in case.
Best,
Alexey
- Couldn't find crash logs. Only this in the console
2024-02-28 18:43:39.008 [ERROR] [io.getstream.logger] [ListDatabaseObserver.swift:258] [startObserving()] > Assert failed: Unable to convert a DB entity to model: ChannelReadDTO object with ID 0xef2919ff575564f2 <x-coredata://4E991088-F4FE-46FA-B610-EC44158F8D07/ChannelReadDTO/p149> is invalid
StreamChat/ListDatabaseObserver.swift:260: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
// Close the WebSocket connection when component dismounts
func disconnectUser(completion: (() -> Void)? = nil) {
chatClient.disconnect {
completion?()
}
}
func logout() {
// Since we don't need the result of logout() response we call subscribe() right here
_ = authRepository.logout().subscribe()
if let _ : Bool = try? Dependencies.standard.persistenceStorage.getValue(storageType: .userDefaults, key: HfConstants.UserDefaults.hasSignedInWithSSO) {
ssoManager.logOutUser()
}
clearAuthValues()
clearStorageValues()
analyticsManager.reset()
pushNotificationsManager.removeAllPendingNotifications()
remoteImageManager.clearCache()
chatManager.disconnectUser {}
}
- No, the client is created only once on the app launch. It's never destroyed. User is disconnected on profile (instance) change or logging out. On logging out we don't pass anything to the completion block.
final class Chat {
static var shared: Chat = {
return Chat()
}()
private(set) var client: ChatClient
private init() {
guard let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
let dict = NSDictionary(contentsOfFile: path),
let apiKey = dict["streamChatApiKey"] as? String else {
fatalError("Stream chat API key not found in Config.plist")
}
let config = ChatClientConfig(apiKey: APIKey(apiKey))
client = ChatClient(config: config)
}
}
- No background mapping used.
- We'll try to reproduce these steps on 4.49.0. Please, review my answers. Could there be any useful information?
Hi @kirsanov-dev,
Do you have any customization of the Channel List that you can share with us? Since we can't reproduce in our side, it could be related to an issue in your integration.
Best,
Nuno
We didn't customize this list. I have just tried to reproduce it and discovered that the easiest way to do it is to create a direct message group with a person who has absolutely no chat history. Absolutely new user. The app should crash on this new user's side.
Hi @kirsanov-dev,
We just tested this, and it seems to work fine. How are you creating these channels? Can you share the code?
Here is our demo working:
NoCrash.mov
There's one slight difference from your demo. I reproduce the issue when I create a dircect 1:1 message without a name. So the name parameter here is set to nil. Here's how we create direct message chat with one or multiple users.
func makeChannelController() throws {
let selectedUserIds = Set(selectedUsers.map(\.id))
channelController = try chatClient.channelController(
createDirectMessageChannelWith: selectedUserIds,
type: .messaging,
extraData: [:]
)
guard let channelController else { return }
channelController.synchronize { [weak self] error in
guard let self else { return }
if error == nil {
self.selectedUsers = []
self.makeChannelInfo()
self.state = .channel
} else {
self.state = .error
}
}
}
@kirsanov-dev, just to confirm, if you create a chat via sending a message, will the app crash?
Even though it should not be an issue and we tested this in our Demo app as well, but creating an empty 1:1 chat (not a group with two members) is not a quite common usecase ๐ค
There's one slight difference from your demo. I reproduce the issue when I create a dircect 1:1 message without a name. So the name parameter here is set to nil. Here's how we create direct message chat with one or multiple users.
func makeChannelController() throws { let selectedUserIds = Set(selectedUsers.map(\.id)) channelController = try chatClient.channelController( createDirectMessageChannelWith: selectedUserIds, type: .messaging, extraData: [:] ) guard let channelController else { return } channelController.synchronize { [weak self] error in guard let self else { return } if error == nil { self.selectedUsers = [] self.makeChannelInfo() self.state = .channel } else { self.state = .error } } }
@kirsanov-dev It should be fine, the name is nil
by the default, so the issue is not related with that.
btw @kirsanov-dev , I can see that in your logout()
function you are not logging out from Stream, you just disconnect the user. You need to call streamChat.logout()
and not disconnect in this case, and ideally wait for the completion block.
@testableapple no, the app crashes instantly after the chat creator presses the create button. As I already mentioned, the issue can occur without any actions from the iOS user. An Android user can cause it by creating a new 1:1 chat. The main conditions are: new user with no chat history, 1:1 chat with nil name, and an open chat list screen on iOS user's side.
@nuno-vieira does logout
method contain disconnection under the hood? If I change user's instance inside the app without logging out, do I have to disconnect AND logout
OR only logout
? New instance means the same email for login but a different employer, another set of permissions and definitely another set of visible chats and channels.
@kirsanov-dev Yes, under the hood it disconnects and cleans the internal DB which is the most important thing here. So you only need to logout, internally we already disconnect too.
Thanks for explanation. I will add logout to our implementation. I'll post update tomorrow. Maybe it will resolve this issue.
The issue still exists. Steps to reproduce:
- Log out using the client.logout { } method.
- Log in as another user without closing the app. The same api key. Name, user id and token are different.
- On successful login, connect using the client.connectUser { } method.
- Open the chat list screen -> the list is updated according to the new user.
Using non-customized built-in component.
ChatChannelListView(
viewFactory: viewModel,
onItemTap: { channel in
viewModel.didPressChannel(
channel: channel,
scrollToMessage: nil
)
},
embedInNavigationView: false
)
- From another phone create a new 1:1 chat with this new user -> the app crashes on new user's phone.
2024-02-29 17:03:43.893 [ERROR] [io.getstream.logger] [ListDatabaseObserver.swift:258] [startObserving()] > Assert failed: Unable to convert a DB entity to model: ChannelReadDTO object with ID 0xdcfd06bdbaef03b2 x-coredata://AA2B30F8-DAA7-419D-8387-9A4007B97D5E/ChannelReadDTO/p1054 is invalid
StreamChat/ListDatabaseObserver.swift:260: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
It seems like there's a conflict in the internal database. This issue never occurs when logging out and then logging back in as the same user.
Please recommend where the root of the problem could be.
Hi @kirsanov-dev,
Some additional questions:
- Does it also happen on iOS + iOS clients or only when the chat was created on Android?
- If you add a name to the channel, will it still crash?
- Do you have any issues creating the channel? Can you add a breakpoint or print an error if it pops up?:
controller.synchronize { [weak self] error in
if let error = error {
print(error.localizedDescription)
return
}
// ...
}
- It may happen on both iOS+Android and iOS+iOS.
- We haven't implemented named channels yet, so I can't test this.
- Added the print statement to synchronize block but didn't catch any errors on creator's phone.
@kirsanov-dev,
- Can you confirm that the crash does not occur if the user has other channels?
- Can you confirm that the crash does not occur if the user creates a channel via sending a message (the way it works on, for instance, WhatsApp)?
@kirsanov-dev, could you please also sniff traffic (e.g. via Proxyman) and check if there is anything suspicious (https/websocket)? What does the participant's phone receive before the crash?
Hi @kirsanov-dev,
Can you test this branch: fix/crash-in-startObserver-after-logout
and see if it fixes it?
@nuno-vieira The issue has been fixed with the changes in the branch fix/crash-in-startObserver-after-logout
. Thank you! This issue can be closed. Will this fix be included in the next release?
P.S. I've noticed another issue: the unread counter briefly appears for a second after every new message and then disappears. Could this problem be related to the recent fix, or is it because I've updated the StreamChatSwiftUI pod to 4.49.0? The counter worked fine on version 4.48.0. Should I open a new issue in stream-chat-swiftui section?
Simulator.Screen.Recording.-.iPhone.14.-.2024-02-29.at.22.39.31.mp4
@kirsanov-dev, thanks for checking the branch. we're glad it solved the crash for you. It was a proof of concept to detect a place where the issue might be. We will share an update here as soon as it will be fixed and released. The PR will be linked to this issue.
Regarding the SwiftUI-related issue, please report it to https://github.com/GetStream/stream-chat-swiftui.
@kirsanov-dev Thanks for testing it out. ๐ Make sure that you don't have a user online on another device reading the channel. The Unread feature it is not changed, so it should work ๐ค But it should not be related to SwiftUI.
If you verified that nobody is reading the channel. Does the unread start working again, if you logout and login again?
Update: I was actually able to reproduce it, if you disconnect and login with a different user, the unread count won't work properly. Even tho the crash has been fixed, you still need to make sure you logout, and not disconnect, if your intent is to login with a different user.
@nuno-vieira
I'm testing on the same fix/crash-in-startObserver-after-logout
branch as yesterday.
I set a breakpoint on this line in the Stream source code
log.debug("Logging out current user successfully.", subsystems: .all)
in the method logout(completion: @escaping () -> Void)
.
It gets called on every logout event.
The logout method in the app calls only logout, not disconnect.
func logout() {
_ = authRepository.logout().subscribe()
...
chatManager.logout {}
clearAuthValues()
clearStorageValues()
analyticsManager.reset()
pushNotificationsManager.removeAllPendingNotifications()
remoteImageManager.clearCache()
}
The issue with the unread counter still occurs.
Log out as user A and re-enter as the same user A -> the unread counter works as usual.
Log out as user A and login as another user B -> the unread counter doesn't appear at all.
Log out as user A, login as user B, log out as B and re-enter to the original user A -> on every new message the unread counter appears for a moment and then disappears.
Hi @kirsanov-dev, we were able to reproduce the issue with unread counter on our side, it's a bug in LLC, so no need to report it to a SwiftUI-related repo. We're investigating it and will keep you posted.
Hi @kirsanov-dev!
Just to let you know, the 2 fixes will be available in the next 4.50.0 release ๐ ETA is next week.
Best,
Nuno