immich-app/immich · error · IOException
Cannot open original stream for asset $assetId
Error message
Cannot open original stream for asset $assetId
What it means
When hashing an asset, the resolver resolves the asset's MediaStore URI (optionally requiring the original file via MediaStore.setRequireOriginal) and opens an InputStream. If ContentResolver.openInputStream returns null — meaning the content provider could not open the file — this IOException is thrown with the asset id.
Solutions
- Verify storage/photo permissions (READ_MEDIA_IMAGES, ACCESS_MEDIA_LOCATION) are granted
- Re-check the asset still exists in MediaStore before hashing and skip/refresh missing entries
- Exclude or re-download cloud-only assets before hashing
- Catch the IOException per-asset so one bad asset does not abort the whole sync
Example fix
// before
final stream = await openOriginalStream(assetId); // throws IOException
// after
try {
final stream = await openOriginalStream(assetId);
...
} on IOException {
markAssetUnreadable(assetId); // skip and continue
} Defensive patterns
Strategy: try-catch
Validate before calling
// Kotlin: verify asset is openable before hashing
val exists = runCatching {
ctx.contentResolver.openAssetFileDescriptor(uri, "r")?.use { true } == true
}.getOrDefault(false)
if (!exists) skipAsset(assetId) Type guard
fun Uri.isReadable(cr: ContentResolver): Boolean =
runCatching { cr.openInputStream(this)?.use { true } == true }.getOrDefault(false) Try / catch
try {
hashAsset(assetId)
} catch (e: IOException) {
Log.w(TAG, "unreadable asset $assetId", e)
markAssetPending(assetId)
} Prevention
- Re-query MediaStore existence before hashing
- Request ACCESS_MEDIA_LOCATION / media permissions at runtime
- Handle cloud-only (evicted) assets separately
- Hash per-asset inside isolated error handling
When it happens
Trigger: openOriginalStream is called during asset hashing and ContentResolver.openInputStream(uri) returns null: the asset row exists in MediaStore but the backing file is missing, or setRequireOriginal original file is inaccessible.
Common situations: Cloud-synced photos whose local bytes were evicted (only placeholder available), assets deleted between query and hash, corrupted MediaStore entries, missing READ_MEDIA_IMAGES/ACCESS_MEDIA_LOCATION permissions causing provider denial.
Understand the failure class
Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.
Related errors
- Could not read image data for $assetId
- Cannot unlink Android motion photos
- could not convert bitmap to ARGB_8888
- EOFException
- Invalid image data
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15).
Data as JSON: /api/errors/193381f38554be20.
Report an issue: GitHub.
Appendix: source
Thrown at mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt:484
HashResult(assetId, "Failed to hash asset: ${e.message}", null)
}
}
private fun openOriginalStream(assetId: String): InputStream {
val id = assetId.toLong()
val collection = when (getMediaType(id)) {
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val uri = ContentUris.withAppendedId(collection, id)
val original = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.setRequireOriginal(uri)
} else {
uri
}
return ctx.contentResolver.openInputStream(original)
?: throw IOException("Cannot open original stream for asset $assetId")
}
private fun getMediaType(id: Long): Int =
getCursor(
MediaStore.VOLUME_EXTERNAL,
"${MediaStore.MediaColumns._ID} = ?",
arrayOf(id.toString()),
arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE)
)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE))
} else {
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
}
} ?: MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
fun cancelHashing() {
hashTask?.cancel()View on GitHub (pinned to f48d4b3321)