Fatal Exception: com.google.protobuf.n1: Protocol message had invalid UTF-8
Description:I'm encountering a recurring crash in my Android app (prod only) related to DataStore and Protocol Buffers. The crash log indicates an issue with invalid UTF-8 during the deserialization of a SettingsPrefs message.
Fatal Exception: com.google.protobuf.n1: Protocol message had invalid UTF-8. at com.google.protobuf.Utf8$DecodeUtil.handleTwoBytes(Utf8.java:1869) at com.google.protobuf.Utf8$DecodeUtil.access$700(Utf8.java:1831) at com.google.protobuf.Utf8$SafeProcessor.decodeUtf8(Utf8.java:978) at com.google.protobuf.Utf8.decodeUtf8(Utf8.java:318) at com.google.protobuf.CodedInputStream$StreamDecoder.readStringRequireUtf8(CodedInputStream.java:2313) at com.google.protobuf.CodedInputStreamReader.readStringRequireUtf8(CodedInputStreamReader.java:143) at com.google.protobuf.MessageSchema.readString(MessageSchema.java:4604) at com.google.protobuf.MessageSchema.mergeFromHelper(MessageSchema.java:3053) at com.google.protobuf.MessageSchema.mergeFrom(MessageSchema.java:2955) at com.google.protobuf.GeneratedMessageLite.parsePartialFrom(GeneratedMessageLite.java:1626) at com.google.protobuf.GeneratedMessageLite.parseFrom(GeneratedMessageLite.java:1762) at com.experiencealula.mobile.settings.persistence.serializer.SettingsPrefs.parseFrom(SettingsPrefs.java:1227) at com.sami.mobile.settings.data.persistence.serializer.SettingsSerializer.readFrom(SettingsSerializer.kt) at androidx.datastore.core.SingleProcessDataStore.readData(SingleProcessDataStore.kt:381) at androidx.datastore.core.SingleProcessDataStore.readDataOrHandleCorruption(SingleProcessDataStore.kt:359) at androidx.datastore.core.SingleProcessDataStore.readAndInit(SingleProcessDataStore.kt:322) at androidx.datastore.core.SingleProcessDataStore.readAndInitOrPropagateAndThrowFailure(SingleProcessDataStore.kt:302) at androidx.datastore.core.SingleProcessDataStore.handleUpdate(SingleProcessDataStore.kt:281) at androidx.datastore.core.SingleProcessDataStore.access$getFile(SingleProcessDataStore.kt) at androidx.datastore.core.SingleProcessDataStore.access$handleUpdate(SingleProcessDataStore.kt) at androidx.datastore.core.SingleProcessDataStore$actor$3.invokeSuspend(SingleProcessDataStore.kt:242) at androidx.datastore.core.SingleProcessDataStore$actor$3.invoke(SingleProcessDataStore.kt:1) at androidx.datastore.core.SingleProcessDataStore$actor$3.invoke(SingleProcessDataStore.kt:2) at androidx.datastore.core.SimpleActor$offer$2.invokeSuspend(SimpleActor.kt:122) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.java:115) at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:100) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.java:584) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Implementation Details:I'm using Android Jetpack Compose with Kotlin to manage settings persistence using DataStore and Protocol Buffers. Here are the relevant parts of my implementation.
SettingsPersistenceModule:
@[Module InstallIn(SingletonComponent::class)]internal object SettingsPersistenceModule { private const val SETTINGS_PREFS = "settings_prefs" private val Context.settingsPrefs: DataStore<SettingsPrefs> by dataStore( fileName = SETTINGS_PREFS, serializer = SettingsSerializer, corruptionHandler = ReplaceFileCorruptionHandler( produceNewData = { SettingsPrefs.getDefaultInstance() } ) ) @[Provides Singleton] internal fun cache( @ApplicationContext context: Context ): DataStore<SettingsPrefs> = context.settingsPrefs}
SettingsSerializer:
internal object SettingsSerializer : Serializer<SettingsPrefs> { override val defaultValue: SettingsPrefs = SettingsPrefs.getDefaultInstance() override suspend fun readFrom(input: InputStream): SettingsPrefs { return try { SettingsPrefs.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { Log.e("SettingsSerializer", "Cannot read proto: ${exception.message}", exception) throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo(t: SettingsPrefs, output: OutputStream) { t.writeTo(output) }}
ProtoFile:
syntax = "proto3";option java_package = "com.sami.mobile.settings.persistence.serializer";option java_multiple_files = true;message SettingsPrefs { bool has_onboarding_seen = 1; string current_region_topic = 2; string language = 3; string current_language_topic = 4; string profile_title = 5; string my_bookings_title = 6; string my_interests_title = 7; string settings_title = 8; string help_title = 9; string faq_title = 10; string contact_title = 11; string preferences_title = 12; string language_title = 13; string push_notification_title = 14; string push_notification_description = 15; string policies_title = 16; EmergencyContactPrefs emergencyContact = 17; AppearanceOptionPrefs appearanceOption = 18; AppearancePrefs appearance = 19; repeated PolicyPrefs policies = 20; int64 review_timer = 21; bool show_in_app_review = 22; string newsletter_title = 23; WeatherConfigPrefs weatherConfigs = 24; string find_booking_title = 25; bool show_login = 26; bool clear_cart = 27;}message EmergencyContactPrefs { string description = 1; string information = 2; repeated string phoneNumbers = 3; string subTitlePopup = 4; string title = 5; string titlePopup = 6;}message AppearancePrefs { string title = 1; string lightModeTitle = 2; string darkModeTitle = 3; string deviceSettingsLabel = 4; string disclaimer = 5;}message PolicyPrefs { string id = 1; string path = 2; string title = 3;}message WeatherConfigPrefs { string profileId = 1; string locationId = 2; string token = 3;}enum AppearanceOptionPrefs { DEVICE_DEFAULT = 0; LIGHT_MODE = 1; DARK_MODE = 2;}