Skip to content

[cloud_functions][android] Error codes are lowercased with the device locale — every code containing "i" breaks on Turkish/Azerbaijani devices #18647

Description

@hsnsaka-source

Is there an existing issue for this?

  • I have searched the existing issues.

Which plugins are affected?

Cloud Functions

Which platforms are affected?

Android

Description

On a device whose locale is Turkish (tr), FirebaseFunctionsException.code comes back with a dotless ı (U+0131) instead of i:

[firebase_functions/faıled-precondıtıon] AI_CONSENT_REQUIRED

Note faıled-precondıtıon. Because of this, the documented comparison

if (e.code == 'failed-precondition') { ... }

is false on those devices, and every switch (e.code) silently falls through to its default branch.

Root causepackages/cloud_functions/cloud_functions/android/src/main/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPlugin.kt:

details["code"] = code.replace("_", "-").lowercase(Locale.getDefault())

code is a FirebaseFunctionsException.Code enum name such as FAILED_PRECONDITION. lowercase(Locale.getDefault()) uses the device locale, and in Turkish and Azerbaijani 'I'.lowercase() is 'ı', not 'i'.

Suggested fix — one word:

details["code"] = code.replace("_", "-").lowercase(Locale.ROOT)

This matches what firebase_auth already does in its own single lowercase call (PhoneNumberVerificationStreamHandler.java uses .toLowerCase(Locale.ROOT)).

Affected codes — every canonical code containing the letter i: failed-precondition, invalid-argument, permission-denied, unauthenticated, internal, unavailable, deadline-exceeded, already-exists, unimplemented.

Codes without i (aborted, cancelled, not-found, data-loss, out-of-range, resource-exhausted, unknown) are unaffected, which is why the bug looks intermittent: an app handles some rejections correctly and silently mis-handles the rest.

Platform divergence

  • Android — affected. The Kotlin line above.
  • iOS — not affected. FirebaseFunctionsPlugin.swift's mapFunctionsErrorCode(_:) returns hardcoded string literals, so no case conversion happens.
  • firebase_auth — not affected. The Android side passes getErrorCode() through unchanged; the conversion happens in Dart (firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart), and Dart's String.toLowerCase() performs Unicode default case mapping and is locale-independent by specification.
  • cloud_firestore — not affected. No case conversion. Observed on the same device in the same session: cloud_firestore/unavailable, with a normal i.

So the same app, same account and same server response behave differently on Android and iOS purely because of the phone's language setting.

Why this matters beyond one app: comparing the error code is the documented way to branch on callable failures, and unit tests do not catch this — test runners use a neutral locale, so the mapping passes everywhere except on users' phones. For an app whose primary market is Turkey this is not an edge case, it is the default path. In our case a server-side consent rejection reached the user as a generic "service unavailable" message instead of the one telling them which setting to enable.

Reproducing the issue

  1. Set the device language to Turkish.
  2. Deploy a callable that throws:
exports.throwing = functions.https.onCall(() => {
  throw new functions.https.HttpsError('failed-precondition', 'X');
});
  1. Call it and print the code:
try {
  await FirebaseFunctions.instance.httpsCallable('throwing').call();
} on FirebaseFunctionsException catch (e) {
  debugPrint(e.code);                                // faıled-precondıtıon
  debugPrint('${e.code == "failed-precondition"}');  // false
}

Expected: e.code == 'failed-precondition' is true on every device locale.
Actual: false on Turkish and Azerbaijani devices.

Firebase Core version

4.12.1

Flutter Version

3.44.4 (stable)

Relevant Log Output

[firebase_functions/faıled-precondıtıon] AI_CONSENT_REQUIRED

Flutter dependencies

cloud_functions: 6.3.5
firebase_core: 4.12.1
firebase_auth: 6.5.6   (unaffected — conversion happens in Dart)

Additional context and comments

Observed on Android 16 (SDK 36), Samsung tablet, device locale tr-TR.

Workaround for anyone hitting this before the fix lands: normalise the code before comparing.

String normalizeCallableCode(String? code) =>
    (code ?? '').toLowerCase().replaceAll('ı', 'i').replaceAll('İ', 'i');

Note the fix on the app side cannot be "lowercase it correctly" — Dart's toLowerCase() is already locale-independent; the string arrives already mangled from the platform channel, so the workaround has to recognise the mangled form.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions