You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
For a media upload file that exists but can't be read (chmod 000), or a directory at the file path, the Swift and Kotlin executors classify the same input as different typed errors: Swift →MediaFileUnreadable, Kotlin →MediaFileNotFound.
Kotlin pre-screens every file field before building the body: canBeUploaded() = exists() && isFile && canRead() (native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt:286). A chmod 000 file fails canRead() and a directory fails isFile, so both throw MediaFileNotFound at WpRequestExecutor.kt:143-144 — before the stream is ever opened.
Swift builds the field with FileManager.default.attributesOfItem(atPath:) (native/swift/Sources/wordpress-api/MultipartForm.swift:63), a stat that needs no read permission on the file, so construction succeeds. The failure surfaces later at serialization (open()/read() → streamStatus == .error / bytesRead < 0, MultipartForm.swift:204-234) as .inaccessibleFile(filePath:), which maps to MediaFileUnreadable (SafeRequestExecutor.swift:652).
The genuinely-absent-file case and the deleted-between-check-and-open case agree across platforms (both MediaFileNotFound, both MediaFileUnreadable respectively). Only the present-but-unreadable / non-regular case diverges.
Impact
A shared cross-platform consumer keying on the error type behaves inconsistently by platform:
User messaging: iOS shows "could not be read", Android shows "not found" — for a file that is sitting right there. The Android copy is actively misleading (it exists).
Analytics: the same root cause double-buckets across MediaFileUnreadable and MediaFileNotFound, so neither platform's rate is comparable.
Retry / recovery: a consumer that treats NotFound as terminal but Unreadable as retryable (e.g. an iCloud-evicted file that may re-materialize — the PR's own motivation) retries on iOS and gives up on Android for identical input.
The input class isn't exotic on mobile: NSFileProtectionComplete files on a locked device, and file-provider / iCloud-evicted items, are all stat-succeeds-but-read-fails.
MediaFileUnreadable is arguably the more correct label here (the file exists; "not found" asserts absence, which is false). So aligning most likely means teaching Kotlin's pre-flight to distinguish exists && !canRead from truly-absent, not regressing Swift back to MediaFileNotFound.
Suggested Fix
Split Kotlin's canBeUploaded() so exists() && isFile() && !canRead() (and a directory) route to MediaFileUnreadable rather than MediaFileNotFound, matching Swift. ⚠️ This touches a pre-flight predicate with its own TOCTOU surface, so it needs its own test.
Summary
chmod 000), or a directory at the file path, the Swift and Kotlin executors classify the same input as different typed errors: Swift →MediaFileUnreadable, Kotlin →MediaFileNotFound.MediaFileNotFound(couldn't be opened at all) vsMediaFileUnreadable(opened, then failed) distinction that Classify multipart mid-read failures asMediaFileUnreadable#1546 introduced — and Classify multipart mid-read failures asMediaFileUnreadable#1546's CHANGELOG states "Both executors produce it," a portability the two paths don't actually deliver for this input.Root Cause
canBeUploaded() = exists() && isFile && canRead()(native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt:286). Achmod 000file failscanRead()and a directory failsisFile, so both throwMediaFileNotFoundatWpRequestExecutor.kt:143-144— before the stream is ever opened.FileManager.default.attributesOfItem(atPath:)(native/swift/Sources/wordpress-api/MultipartForm.swift:63), astatthat needs no read permission on the file, so construction succeeds. The failure surfaces later at serialization (open()/read()→streamStatus == .error/bytesRead < 0,MultipartForm.swift:204-234) as.inaccessibleFile(filePath:), which maps toMediaFileUnreadable(SafeRequestExecutor.swift:652).MediaFileNotFound, bothMediaFileUnreadablerespectively). Only the present-but-unreadable / non-regular case diverges.Impact
A shared cross-platform consumer keying on the error type behaves inconsistently by platform:
MediaFileUnreadableandMediaFileNotFound, so neither platform's rate is comparable.NotFoundas terminal butUnreadableas retryable (e.g. an iCloud-evicted file that may re-materialize — the PR's own motivation) retries on iOS and gives up on Android for identical input.The input class isn't exotic on mobile:
NSFileProtectionCompletefiles on a locked device, and file-provider / iCloud-evicted items, are allstat-succeeds-but-read-fails.Notes
canBeUploaded()predicate predates Classify multipart mid-read failures asMediaFileUnreadable#1546; before fix: surface a failed multipart stream open as an error, not a silent truncation #1545/Classify multipart mid-read failures asMediaFileUnreadable#1546, Swift crashed on a directory (Data(bytesNoCopy:count:-1)traps) and silently truncated achmod 000upload. So this is strictly better than the prior state — just still not aligned.MediaFileUnreadableis arguably the more correct label here (the file exists; "not found" asserts absence, which is false). So aligning most likely means teaching Kotlin's pre-flight to distinguishexists && !canReadfrom truly-absent, not regressing Swift back toMediaFileNotFound.Suggested Fix
canBeUploaded()soexists() && isFile() && !canRead()(and a directory) route toMediaFileUnreadablerather thanMediaFileNotFound, matching Swift.MediaFileUnreadable#1546's CHANGELOG line so it stops promising "Both executors produce it" for a present-but-unreadable file until the classification is actually aligned.Related issues
MediaFileUnreadable#1546 (fixes Swift: multipart mid-read failures surface as .genericError, dropping the file path and media classification #1541). Part of the Swift executor error audit (Swift executor URLSession error audit: wrong conversions, unchecked codes, and crash-instead-of-error paths #1497).