fix(blossom): preserve upstream path prefix in xs= hint

The bridge was emitting `xs=https://cdn.nostr.build` for URLs like
`https://cdn.nostr.build/i/<sha>.jpg`, dropping the `/i/` prefix. The
local cache appends `/<sha>` to the xs server hint per spec, so it would
try to fetch `https://cdn.nostr.build/<sha>` — a 404 on nostr.build's
non-Blossom CDN scheme.

Anchor the server-base extraction on the slash immediately preceding the
sha-bearing path segment so we emit `xs=https://cdn.nostr.build/i`.
The cache then appends `/<sha>` and reaches the original blob.

Affects all three bridge entry points:
- MediaUrlContent.toCoilModel (note media)
- bridgeProfilePictureUrl (profile pictures)
- LocalBlossomCacheRedirectInterceptor (everything else via OkHttp)

Flat Blossom paths (`<host>/<sha>`) keep emitting `xs=<host>` unchanged.
This commit is contained in:
Claude
2026-05-08 11:35:53 +00:00
parent aa598b9949
commit bb871f6c71
4 changed files with 94 additions and 14 deletions
@@ -61,26 +61,42 @@ class LocalBlossomCacheRedirectInterceptor(
val host = url.host
if (host == LOCAL_CACHE_HOST || host.equals("localhost", ignoreCase = true)) return null
val (sha, ext) = findSha256AndExtensionInPath(url) ?: return null
val originalHostBase = "${url.scheme}://$host" + if (url.port != HttpUrl.defaultPort(url.scheme)) ":${url.port}" else ""
val (shaSegmentIndex, sha, ext) = findSha256AndExtensionInPath(url) ?: return null
val serverBase = buildServerBase(url, shaSegmentIndex)
return "$LOCAL_CACHE_BASE/$sha.$ext"
.toHttpUrl()
.newBuilder()
.addQueryParameter("xs", originalHostBase)
.addQueryParameter("xs", serverBase)
.build()
}
private fun findSha256AndExtensionInPath(url: HttpUrl): Pair<String, String>? {
for (segment in url.pathSegments) {
val match = SHA256_SEGMENT_REGEX.find(segment) ?: continue
private fun findSha256AndExtensionInPath(url: HttpUrl): Triple<Int, String, String>? {
url.pathSegments.forEachIndexed { index, segment ->
val match = SHA256_SEGMENT_REGEX.find(segment) ?: return@forEachIndexed
val sha = match.value.lowercase()
val ext = guessExtensionFrom(segment, sha) ?: "bin"
return sha to ext
return Triple(index, sha, ext)
}
return null
}
/**
* Builds the URL prefix that the local cache should append `/<sha>` to.
* Preserves any path prefix the upstream CDN uses (e.g. `/i` for
* `https://cdn.nostr.build/i/<sha>`) so the cache can fetch the blob
* from its actual location on miss.
*/
private fun buildServerBase(
url: HttpUrl,
shaSegmentIndex: Int,
): String {
val origin = "${url.scheme}://${url.host}" + if (url.port != HttpUrl.defaultPort(url.scheme)) ":${url.port}" else ""
if (shaSegmentIndex == 0) return origin
val prefixSegments = url.pathSegments.subList(0, shaSegmentIndex)
return "$origin/" + prefixSegments.joinToString("/")
}
private fun guessExtensionFrom(
segment: String,
sha: String,
@@ -114,7 +114,19 @@ class LocalBlossomCacheRedirectInterceptorTest {
val captured = mutableListOf<String>()
val response = interceptor.intercept(fakeChain("https://nostr.build/i/cache/$sha.webp", captured))
assertEquals(
"http://127.0.0.1:24242/$sha.webp?xs=https%3A%2F%2Fnostr.build",
"http://127.0.0.1:24242/$sha.webp?xs=https%3A%2F%2Fnostr.build%2Fi%2Fcache",
captured.single(),
)
response.close()
}
@Test
fun bridgeOnPreservesNostrBuildPathPrefix() {
val interceptor = LocalBlossomCacheRedirectInterceptor { true }
val captured = mutableListOf<String>()
val response = interceptor.intercept(fakeChain("https://cdn.nostr.build/i/$sha.jpg", captured))
assertEquals(
"http://127.0.0.1:24242/$sha.jpg?xs=https%3A%2F%2Fcdn.nostr.build%2Fi",
captured.single(),
)
response.close()