Merge pull request #2959 from greenart7c3/claude/fix-blossom-cache-url-8zolt

fix(blossom): only bridge to local cache when URL is BUD-01 layout
This commit is contained in:
Vitor Pamplona
2026-05-18 08:49:22 -04:00
committed by GitHub
2 changed files with 36 additions and 10 deletions
@@ -104,13 +104,20 @@ private fun bridgeUrl(
if (url.startsWith("blossom:", ignoreCase = true)) return url
if (!url.startsWith("http://", ignoreCase = true) && !url.startsWith("https://", ignoreCase = true)) return url
val sha =
explicitHash?.lowercase()?.takeIf { sha256HexRegex.matches(it) }
?: extractSha256FromUrlPath(url)
?: return url
// The local Blossom cache fetches `<xs>/<sha>.<ext>` on miss per BUD-01,
// which only works when the upstream URL is itself BUD-01 layout. For
// non-BUD-01 URLs (e.g. https://i.nostr.build/M5AwJ.gif) the imeta `x`
// hash identifies the blob but the upstream server doesn't host it at
// /<sha>.<ext>, so trusting only `explicitHash` would point the cache
// at a 404. Require the sha to be in the URL path before bridging.
val urlSha = extractSha256FromUrlPath(url) ?: return url
// Prefer the imeta hash when it's a valid sha256 (authoritative casing),
// otherwise fall back to what was parsed from the URL.
val sha = explicitHash?.lowercase()?.takeIf { sha256HexRegex.matches(it) } ?: urlSha
val ext = guessExtension(url, mimeType)
val serverBase = extractServerBase(url, sha) ?: return url
val serverBase = extractServerBase(url, urlSha) ?: return url
val authors =
authorPubKey
@@ -80,14 +80,15 @@ class MediaUrlContentExtTest {
@Test
fun bridgeOnInfersExtensionFromMimeType() {
val image = MediaUrlImage(url = "https://nostr.build/i/abc", hash = sha, mimeType = "image/png")
// BUD-01 allows `<sha>` without an extension; mimeType supplies one.
val image = MediaUrlImage(url = "https://nostr.build/i/$sha", hash = sha, mimeType = "image/png")
val result = image.toCoilModel(useLocalBlossomBridge = true)
assertTrue(result.startsWith("blossom:$sha.png?xs="), "expected png extension from mime, got $result")
}
@Test
fun bridgeOnFallsBackToBinExtension() {
val image = MediaUrlImage(url = "https://nostr.build/i/abc", hash = sha)
val image = MediaUrlImage(url = "https://nostr.build/i/$sha", hash = sha)
val result = image.toCoilModel(useLocalBlossomBridge = true)
assertTrue(result.startsWith("blossom:$sha.bin?xs="), "expected bin extension fallback, got $result")
}
@@ -100,7 +101,7 @@ class MediaUrlContentExtTest {
@Test
fun uppercaseHashNormalisedToLowercase() {
val image = MediaUrlImage(url = "https://cdn.example.com/foo.jpg", hash = sha.uppercase())
val image = MediaUrlImage(url = "https://cdn.example.com/${sha.uppercase()}.jpg", hash = sha.uppercase())
val result = image.toCoilModel(useLocalBlossomBridge = true)
assertTrue(result.startsWith("blossom:$sha.jpg?xs="), "expected lowercase sha, got $result")
}
@@ -110,7 +111,7 @@ class MediaUrlContentExtTest {
val authorPub = "a8f3721a0dc1b4d5c12f4cc7c54ae14071eb9c1b4f9b2cf0d4ab22c0e9f0c7e5"
val image =
MediaUrlImage(
url = "https://cdn.example.com/foo.jpg",
url = "https://cdn.example.com/$sha.jpg",
hash = sha,
authorPubKey = authorPub,
)
@@ -122,7 +123,7 @@ class MediaUrlContentExtTest {
fun invalidAuthorPubKeyDropped() {
val image =
MediaUrlImage(
url = "https://cdn.example.com/foo.jpg",
url = "https://cdn.example.com/$sha.jpg",
hash = sha,
authorPubKey = "not-a-pubkey",
)
@@ -130,6 +131,24 @@ class MediaUrlContentExtTest {
assertEquals("blossom:$sha.jpg?xs=https://cdn.example.com", result)
}
@Test
fun bridgeOnSkipsNonBud01UrlEvenWithImetaHash() {
// The imeta `x` hash refers to a blob whose canonical Blossom location
// is /<sha>.<ext>, but the upstream URL serves it under a different
// path (https://i.nostr.build/M5AwJ.gif). Routing this through the
// local cache would set xs=https://i.nostr.build, and the cache would
// fetch https://i.nostr.build/<sha>.gif on miss, which 404s.
val url = "https://i.nostr.build/M5AwJ.gif"
val image = MediaUrlImage(url = url, hash = sha)
assertEquals(url, image.toCoilModel(useLocalBlossomBridge = true))
}
@Test
fun bridgeProfilePictureUrlSkipsNonBud01UrlEvenWithImetaHash() {
val url = "https://i.nostr.build/M5AwJ.gif"
assertEquals(url, bridgeProfilePictureUrl(url, useBridge = true))
}
@Test
fun bridgeProfilePictureUrlNullReturnsNull() {
assertEquals(null, bridgeProfilePictureUrl(null, useBridge = true))