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;
}