firebase / firebase/firebase-android-sdk
Crashlytics NDK upload doesn't cleanup previous run's intermediate files when upload fails
- Dominant language
- Java
- Stars
- 2.6k
- Forks
- 710
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 34
Description
### Describe your environment
- Android Studio version: Narwhal | 2025.1
- Firebase Component: Crashlytics NDK (gradle plugin + buildtools)
- Component version: `com.google.firebase:firebase-crashlytics-gradle:3.0.7` (also reproducible on 3.0.6), AGP 8.x, Gradle 8.13, JDK 17
### Describe the problem
I have `uploadCrashlyticsSymbolFileRelease` set to run after `bundleRelease` (was necessary since the plugin does not auto-upload #7582).
While uploading the symbols, hit a server error :
```
> java.io.IOException: Unknown error while sending file, check network [//build/crashlytics/Release/nativeSymbols/.gz; response: 503 HTTP/1.1 503 Service Unavailable]
```
I retried and it failed again with error :
```
> java.io.FileNotFoundException: //build/crashlytics/Release/nativeSymbols/.gz (No such file or directory)
```
I then ran `rm -rf ./build/crashlytics` which then completed successfully.
#### Steps to reproduce:
1. Set finalizedBy `uploadCrashlyticsSymbolFileRelease` on `bundleRelease`:
```groovy
tasks.configureEach { task ->
def m = task.name =~ /^bundle(.+)Release$/
if (m) task.finalizedBy("uploadCrashlyticsSymbolFile" + m[0][1] + "Release")
}
```
2. Run `./gradlew bundleRelease` which hits 503 from upload task.
3. Without making any change, rerun the same command
#### Relevant Code:
1. **No `try/finally` in `FirebaseSymbolFileService.uploadNativeSymbolFile`**
([source](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-crashlytics-buildtools/src/main/java/com/google/firebase/crashlytics/buildtools/api/FirebaseSymbolFileService.java)).
`gZippedSymbolFile.delete()` is sequential code after `webApi.uploadFile()` — when `uploadFile`
throws on a 503, the `.gz` is never deleted and is left as an orphan in the output directory.
2. **No `try/finally` in `Buildtools.uploadNativeSymbolFiles`**
([source](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-crashlytics-buildtools/src/main/java/com/google/firebase/crashlytics/buildtools/Buildtools.java)).
`symbolFile.delete()` also does not run on exception, and the for-loop aborts on the first
failure — every `.sym` that hadn't yet been processed is also stranded.
3. **No filter on `dir.listFiles()` in `Buildtools.uploadNativeSymbolFiles`**
([source](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-crashlytics-buildtools/src/main/java/com/google/firebase/crashlytics/buildtools/Buildtools.java)): the next run iterates every entry
in `nativeSymbols/`, including the orphan `.gz`. For an input named `orphan.gz`, the code computes
`gZippedSymbolFile = parent + removeExtension("orphan.gz") + ".gz"` = **same path as the input**,
and `FileUtils.gZipFile(orphan.gz, orphan.gz)` truncate-rewrites the file while a
`FileInputStream` on it is still open.
4. `RestfulWebApi.uploadFile` ([source](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-crashlytics-buildtools/src/main/java/com/google/firebase/crashlytics/buildtools/api/RestfulWebApi.java)) makes a single attempt with no retry on 5xx, so a transient backend issue fails the build.
#### Workaround that I'm using to delete previous run's possible orphans :
```groovy
tasks.matching { it.name.startsWith("uploadCrashlyticsSymbolFile") }.configureEach {
doFirst {
def dir = symbolFileDirectory.get().asFile
if (dir.exists()) {
dir.listFiles({ f -> !f.name.endsWith(".sym") } as FileFilter)
?.each { it.delete() }
}
}
}
```
Contributor guide
Assessment
This issue has not been assessed yet.