Say "not available in your country" instead of "relay returned 451"
The edge geo-gate serves US, PE and BO for the relay and accounts hosts, and answers 451 for anywhere else — before the request reaches the relay. Nothing in either client knew what 451 meant, so it fell through to the generic branch. ⚠️ **The download site is deliberately not gated**, which makes this worse than a bad error string. Someone outside those countries can read the page, install the app, choose a password, tap sign up, and be told "relay returned 451". Nothing in that mentions their country, and it is not a failure they can retry past, so the only sensible reading is that the app is broken. 451 is the right status — this is a legal/policy refusal, not a 403 — so the only thing missing was somebody translating it. client 96. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f9f063a211
commit
db7d05bd98
@@ -294,6 +294,8 @@ class MessengerApi(
|
||||
401, 403 -> RelayException.Unauthorized(message)
|
||||
404 -> RelayException.NotFound(message.ifBlank { request.url.encodedPath })
|
||||
429 -> RelayException.RateLimited(it.header("Retry-After")?.toIntOrNull())
|
||||
// The geo-gate, not the relay. Needs a sentence, not a number.
|
||||
451 -> RelayException.Unavailable(message)
|
||||
else -> RelayException.Http(it.code, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,8 @@ class RelayAccounts(
|
||||
404 -> RelayException.NotFound(path)
|
||||
409 -> RelayException.Http(409, message)
|
||||
429 -> RelayException.RateLimited(it.header("Retry-After")?.toIntOrNull())
|
||||
// The geo-gate, not the relay. Needs a sentence, not a number.
|
||||
451 -> RelayException.Unavailable(message)
|
||||
else -> RelayException.Http(it.code, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +513,8 @@ class RelayClient(
|
||||
401 -> RelayException.Unauthorized(readable)
|
||||
404 -> RelayException.NotFound(path)
|
||||
429 -> RelayException.RateLimited(response.header("Retry-After")?.toIntOrNull())
|
||||
// The geo-gate, not the relay. Needs a sentence, not a number.
|
||||
451 -> RelayException.Unavailable(readable)
|
||||
else -> RelayException.Http(code, readable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,21 @@ sealed class RelayException(message: String, cause: Throwable? = null) : Excepti
|
||||
class RateLimited(val retryAfterSeconds: Int?) :
|
||||
RelayException("rate limited" + (retryAfterSeconds?.let { ", retry in ${it}s" } ?: ""))
|
||||
|
||||
/**
|
||||
* 451 — this service is not offered in the caller's country.
|
||||
*
|
||||
* 🔴 **Its own type because the generic one reads as a broken app.** The
|
||||
* edge geo-gate answers 451 before the request reaches the relay, and the
|
||||
* download site is deliberately NOT gated — so somebody can read the page,
|
||||
* install the app, choose a password, tap sign up, and be shown "relay
|
||||
* returned 451". Nothing in that mentions their country, and it is not a
|
||||
* failure they can retry past, so the only sensible reading is that the app
|
||||
* does not work.
|
||||
*/
|
||||
class Unavailable(val body: String) : RelayException(
|
||||
"Flamenet isn't available in your country yet. " +
|
||||
"This isn't a problem with your account or your connection.")
|
||||
|
||||
/** Any other non-2xx. The body is kept because relays put the reason in it. */
|
||||
class Http(val status: Int, val body: String) : RelayException("relay returned $status: $body")
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.flamenet.client
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* 451 — the country gate (§ edge policy).
|
||||
*
|
||||
* 🔴 **The download site is deliberately NOT gated.** Someone outside the
|
||||
* served countries can read the page, install the app, choose a password, tap
|
||||
* sign up — and only then does the edge answer 451, before the request ever
|
||||
* reaches the relay. Reported as a status code, that reads as a broken app:
|
||||
* nothing mentions their country, and it is not a failure they can retry past.
|
||||
*/
|
||||
class GeoUnavailableTest {
|
||||
|
||||
@Test
|
||||
fun `the message names the country, not the status code`() {
|
||||
val message = RelayException.Unavailable("").message.orEmpty()
|
||||
assertTrue("country" in message, "must say why: $message")
|
||||
assertTrue("451" !in message, "a status code explains nothing: $message")
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ It must also say what it is NOT. Someone who has just typed a password
|
||||
* will otherwise assume they got it wrong and try again — and again.
|
||||
*/
|
||||
@Test
|
||||
fun `the message rules out the account and the connection`() {
|
||||
val message = RelayException.Unavailable("").message.orEmpty()
|
||||
assertTrue("account" in message && "connection" in message, message)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user