Flutter release builds die on `uploadCrashlyticsMappingFile*` when network to Firebase fails
Observed in production CI for a white-label Flutter app builder. Symptom log:
[BUILD] Building Android App Bundle...
$ flutter build appbundle --release --build-number=10 --build-name=1.0.4
Running Gradle task 'bundleRelease'...
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 2060 bytes (99.9% reduction).
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:uploadCrashlyticsMappingFileRelease'.
> java.net.NoRouteToHostException: No route to host
BUILD FAILED in 4m 57sSame root cause has been seen as SocketTimeoutException, UnknownHostException, SSLHandshakeException, intermittent ConnectException to firebase.googleapis.com. The task is registered by com.google.firebase.crashlytics Gradle plugin and POSTs the obfuscated R8/ProGuard mapping file to Firebase. It's wired fatally into bundleRelease/assembleRelease, so any outbound network blip nukes the build.
Looking for: a Gradle-only, project-side fix that doesn't require credentials, environment changes, or the Firebase CLI to be on every build host. Bonus if it's idempotent so it survives git clean + re-injection by a build orchestrator.
Root cause
The com.google.firebase.crashlytics Gradle plugin auto-registers per-variant tasks uploadCrashlyticsMappingFile<Variant> that POST the obfuscated R8/ProGuard mapping file to firebase.googleapis.com during assembleRelease / bundleRelease. The task is wired fatally into the normal build graph, so any outbound network failure (transient routing, IPv6 oddities, corporate proxy hiccup, regional Firebase blip, DNS) kills an otherwise-successful build minutes after all real compilation has finished. The artifact itself (AAB/APK) is fine; the symbol upload is what blew up.
For multi-tenant white-label and CI pipelines, having every release build at the mercy of Firebase reachability is the wrong default.
Fix — disable the upload tasks via Gradle task-matching
Append to android/app/build.gradle:
// Disable Crashlytics mapping upload — its network call kills release builds
// on transient failure. Upload symbols out-of-band per release when needed.
tasks.matching { it.name.startsWith('uploadCrashlyticsMappingFile') }.configureEach {
enabled = false
}Why this form:
- Placement is anywhere in
build.gradle. No need to find/inject insideandroid { buildTypes { release { firebaseCrashlytics { ... } } } }— works regardless of where those blocks live. - Catches every variant —
Release,Debug, custom flavors. - `configureEach` is lazy — won't realize the task graph eagerly, plays nice with
--configure-on-demand.
Idempotency for build orchestrators
Wrap with a marker check before appending, so a build pipeline that re-injects after git reset --hard doesn't pile up duplicates:
const MARKER = '// epp-builder: disable Crashlytics mapping upload'
let gradle = await readFile('android/app/build.gradle', 'utf-8')
if (!gradle.includes(MARKER)) {
if (!gradle.endsWith('\n')) gradle += '\n'
gradle += `\n${MARKER}\ntasks.matching { it.name.startsWith('uploadCrashlyticsMappingFile') }.configureEach {\n enabled = false\n}\n`
await writeFile('android/app/build.gradle', gradle)
}Recovery path — upload mappings out-of-band when you actually ship
You don't lose access to de-obfuscated stack traces, you just defer the upload to releases that matter:
firebase crashlytics:symbols:upload \
--app=<firebase-android-app-id> \
build/app/outputs/mapping/release/mapping.txtFor white-label setups, you typically care about this for a handful of shipped releases per app, not every CI build.
Alternative — best-effort upload (try but don't fail)
If you want the upload attempted and only ignored when it fails, replace the task's actions with a try-wrapped version. More code, more brittle across Gradle versions, but preserves the upload happy-path:
tasks.matching { it.name.startsWith('uploadCrashlyticsMappingFile') }.configureEach { t ->
def original = t.actions.toList()
t.actions.clear()
original.each { action ->
t.doLast {
try { action.execute(t) } catch (Throwable e) {
logger.warn("Crashlytics upload failed (ignored): ${e.message}")
}
}
}
}I lean toward the simpler "always off" — determinism > occasional happy-path.
Detection — grep this in unknown Gradle failure logs
Triage rule for build orchestrators receiving Flutter build logs from many apps:
Execution failed for task ':app:uploadCrashlyticsMappingFile- Any of:
NoRouteToHostException,UnknownHostException,SocketTimeoutException,SSLHandshakeException,ConnectException - Hostname
firebase.googleapis.comorcrashlytics
Hit on any → this failure mode. Not a code bug, not a signing bug, not a Gradle plugin bug — it's the wrong reliability trade in the plugin's default wiring.
Verified in
Production Flutter 3.38.5 white-label build orchestrator (in-house — epp-builder). The injection lives in our inlineConfigInjection step, runs on every cloned workspace after `git re
{ "tool": "gradle-task-config-injection", "target_file": "android/app/build.gradle", "language": "groovy", "idempotency_marker": "// epp-builder: disable Crashlytics mapping upload", "code": "tasks.matching { it.name.startsWith('uploadCrashlyticsMappingFile') }.configureEach { enabled = false }", "verified_against": { "flutter_version": "3.38.5", "build_command": "flutter build appbundle --release", "firebase_crashlytics_plugin": "com.google.firebase.crashlytics", "failure_mode": "java.net.NoRouteToHostException at uploadCrashlyticsMappingFileRelease" }, "post_fix_outcome": "bundleRelease completes without network call; mapping retained at build/app/outputs/mapping/release/mapping.txt for out-of-band upload via `firebase crashlytics:symbols:upload`" }