fail a download whose payload is a server error page

A Subsonic server answers a refused download with HTTP 200 and an XML error
document, which the platform downloader saves like any payload, so the row went
Completed and playback then tried to decode the error page. No audio container
starts with markup, so the leading bytes tell them apart: a Completed download
that starts with < or { is failed instead, and a stored Completed row is
re-checked on reconcile so existing bad files get cleaned up too.
This commit is contained in:
austinried
2026-10-04 00:30:03 +00:00
parent f173f394fb
commit 26b8963205
4 changed files with 120 additions and 4 deletions
@@ -0,0 +1,42 @@
package com.subtracks.data.download
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class AudioFileTest {
@Test
fun anXmlErrorDocumentIsNotAudio() {
assertFalse(file("<?xml version=\"1.0\"?><subsonic-response status=\"failed\"/>").looksLikeAudio())
}
@Test
fun leadingWhitespaceAndABomDoNotHideMarkup() {
assertFalse(file("\uFEFF\n <html><body>Gateway timeout</body></html>").looksLikeAudio())
}
@Test
fun aJsonErrorBodyIsNotAudio() {
assertFalse(file("{\"subsonic-response\":{\"status\":\"failed\"}}").looksLikeAudio())
}
@Test
fun anEmptyFileIsNotAudio() {
assertFalse(file("").looksLikeAudio())
}
@Test
fun aMissingFileIsNotAudio() {
assertFalse(File.createTempFile("absent", ".tmp").apply { delete() }.looksLikeAudio())
}
@Test
fun audioHeadersPass() {
assertTrue(file("audio").looksLikeAudio())
assertTrue(file("ID3\u0004\u0000audio").looksLikeAudio())
assertTrue(file("fLaC\u0000\u0000\u0000\u0022").looksLikeAudio())
}
private fun file(contents: String): File = File.createTempFile("audio", ".tmp").apply { writeText(contents) }.also { it.deleteOnExit() }
}
@@ -920,6 +920,42 @@ class DownloadRepositoryTest {
await { row(2, "o1")?.status == DownloadStatus.Completed }
}
@Test
fun aServerErrorSavedAsThePayloadIsNotTreatedAsADownload() =
runTest {
seedLibrary()
repository.start()
runBlocking { repository.download(1, "s1") }
val id = engine.requests.single().first
engine.complete(id, total = 100)
writeFile(
1,
"s1",
"<?xml version=\"1.0\"?><subsonic-response status=\"failed\"><error code=\"70\"/></subsonic-response>",
)
runBlocking { repository.reconcile() }
assertEquals(DownloadStatus.Failed, row(1, "s1")?.status)
assertEquals("The server sent an error instead of the track", row(1, "s1")?.error)
assertFalse(file(1, "s1").exists())
assertNull(repository.localUri("s1"))
}
@Test
fun aCompletedRowLeftAsAnErrorPageIsFailedOnReconcile() =
runTest {
seedLibrary()
runBlocking { db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, bytes = 10, total = 10)) }
writeFile(1, "s1", "<subsonic-response status=\"failed\"/>")
runBlocking { repository.reconcile() }
assertEquals(DownloadStatus.Failed, row(1, "s1")?.status)
assertFalse(file(1, "s1").exists())
assertNull(repository.localUri("s1"))
}
private fun requestedSongIds(): List<String> = engine.requests.map { it.second.path.substringAfterLast('/') }
private fun seedSongs(tracks: IntRange) {