From 7ebd8ed932f2cadc9c99ff4fd2c9844c010f88d1 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Fri, 19 Jun 2026 02:34:39 +0900 Subject: [PATCH] Fix Bluesky AppView proxy header --- .../data/network/bluesky/BlueskyService.kt | 16 +++-- .../network/bluesky/AtprotoProxyPluginTest.kt | 65 +++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 social/bluesky/src/commonTest/kotlin/dev/dimension/flare/data/network/bluesky/AtprotoProxyPluginTest.kt diff --git a/social/bluesky/src/commonMain/kotlin/dev/dimension/flare/data/network/bluesky/BlueskyService.kt b/social/bluesky/src/commonMain/kotlin/dev/dimension/flare/data/network/bluesky/BlueskyService.kt index 071dffaf5..79da41a38 100644 --- a/social/bluesky/src/commonMain/kotlin/dev/dimension/flare/data/network/bluesky/BlueskyService.kt +++ b/social/bluesky/src/commonMain/kotlin/dev/dimension/flare/data/network/bluesky/BlueskyService.kt @@ -91,7 +91,7 @@ internal data class BlueskyService private constructor( fun newBaseUrlService(baseUrl: String): BlueskyService = copy(baseUrlFlow = flowOf(baseUrl)) } -private class AtprotoProxyPlugin { +internal class AtprotoProxyPlugin { companion object : HttpClientPlugin { override val key = AttributeKey("AtprotoProxyPlugin") @@ -102,11 +102,15 @@ private class AtprotoProxyPlugin { scope: HttpClient, ) { scope.requestPipeline.intercept(HttpRequestPipeline.State) { - if (context.url.pathSegments - .lastOrNull() - ?.startsWith("chat.bsky.convo.") == true - ) { - context.headers["Atproto-Proxy"] = "did:web:api.bsky.chat#bsky_chat" + val method = context.url.pathSegments.lastOrNull() + when { + method?.startsWith("app.bsky.") == true -> { + context.headers["Atproto-Proxy"] = "did:web:api.bsky.app#bsky_appview" + } + + method?.startsWith("chat.bsky.convo.") == true -> { + context.headers["Atproto-Proxy"] = "did:web:api.bsky.chat#bsky_chat" + } } } } diff --git a/social/bluesky/src/commonTest/kotlin/dev/dimension/flare/data/network/bluesky/AtprotoProxyPluginTest.kt b/social/bluesky/src/commonTest/kotlin/dev/dimension/flare/data/network/bluesky/AtprotoProxyPluginTest.kt new file mode 100644 index 000000000..2d0fa8ee7 --- /dev/null +++ b/social/bluesky/src/commonTest/kotlin/dev/dimension/flare/data/network/bluesky/AtprotoProxyPluginTest.kt @@ -0,0 +1,65 @@ +package dev.dimension.flare.data.network.bluesky + +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.request.get +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class AtprotoProxyPluginTest { + @Test + fun addsAppViewProxyHeaderForAppBskyMethods() = + runTest { + assertEquals( + "did:web:api.bsky.app#bsky_appview", + proxyHeaderFor("https://pds.test/xrpc/app.bsky.feed.getTimeline"), + ) + } + + @Test + fun keepsChatProxyHeaderForChatMethods() = + runTest { + assertEquals( + "did:web:api.bsky.chat#bsky_chat", + proxyHeaderFor("https://pds.test/xrpc/chat.bsky.convo.listConvos"), + ) + } + + @Test + fun doesNotProxyComAtprotoMethods() = + runTest { + assertNull(proxyHeaderFor("https://pds.test/xrpc/com.atproto.server.describeServer")) + } + + private suspend fun proxyHeaderFor(url: String): String? { + var proxyHeader: String? = null + val client = + HttpClient( + MockEngine { request -> + proxyHeader = request.headers["Atproto-Proxy"] + respond( + content = """{"ok":true}""", + headers = + Headers.build { + append(HttpHeaders.ContentType, "application/json") + }, + ) + }, + ) { + install(AtprotoProxyPlugin) + } + + try { + client.get(url) + } finally { + client.close() + } + + return proxyHeader + } +}