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

  1. Verify storage/photo permissions (READ_MEDIA_IMAGES, ACCESS_MEDIA_LOCATION) are granted
  2. Re-check the asset still exists in MediaStore before hashing and skip/refresh missing entries
  3. Exclude or re-download cloud-only assets before hashing
  4. 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

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


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)