From b051814a6cae2ea69bd797ff7a93dbcea937c952 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Tue, 25 Aug 2026 19:54:19 -0400 Subject: [PATCH] Show the relay's sentence, not its JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creating a group with a member who has never signed in put this on screen: relay returned 400: {"code":"bad_request","message":"No reachable device for user: 30.","data":{"status":400}} The relay had written a perfectly readable sentence and the app showed the envelope around it. Found by creating a group on an emulator, not by reading code. ⚠️ The REFUSAL is correct — requireReachable declines a group whose member has no device, because there is nothing to encrypt to. Only the presentation was wrong. Cause: MessengerApi.send and RelayAccounts already unwrap `message` from the error body; RelayClient.checked did not. So which client a feature happened to be built on decided whether its errors were legible, which is not a decision anyone made. Now all three unwrap. 182 Android unit tests still green. Co-Authored-By: Claude Opus 5 --- .../kotlin/io/flamenet/client/RelayClient.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/client/src/main/kotlin/io/flamenet/client/RelayClient.kt b/client/src/main/kotlin/io/flamenet/client/RelayClient.kt index add1f7d..e586028 100644 --- a/client/src/main/kotlin/io/flamenet/client/RelayClient.kt +++ b/client/src/main/kotlin/io/flamenet/client/RelayClient.kt @@ -421,11 +421,26 @@ class RelayClient( val body = response.body?.string().orEmpty() val code = response.code response.close() + + // ⚠️ **Carry the relay's own sentence, not its JSON.** These messages + // reach the screen: creating a group with a member who has never signed + // in showed the user + // + // relay returned 400: {"code":"bad_request","message":"No reachable + // device for user: 30.","data":{"status":400}} + // + // when the relay had written a perfectly readable sentence inside it. + // `MessengerApi.send` and `RelayAccounts` already unwrap `message`; + // this path did not, so which client a feature happened to use decided + // whether its errors were legible. + val readable = runCatching { JSONObject(body).optString("message") } + .getOrNull().orEmpty().ifBlank { body } + throw when (code) { - 401 -> RelayException.Unauthorized(body) + 401 -> RelayException.Unauthorized(readable) 404 -> RelayException.NotFound(path) 429 -> RelayException.RateLimited(response.header("Retry-After")?.toIntOrNull()) - else -> RelayException.Http(code, body) + else -> RelayException.Http(code, readable) } }