fix(audio-rooms): retry mintToken on transport hiccup + harness /health warmup

The remaining interop failures all root in the same window: a stale
keep-alive pool entry from one test class is reused on the FIRST POST
of the next test class, the connection RSTs as the request body
writes, and OkHttp's built-in retryOnConnectionFailure won't retry a
POST after any byte of the body has gone out. Same situation hits a
phone client whose Wi-Fi hands off mid-mint.

Two fixes, both production-shaped:

  - OkHttpNestsClient.mintToken now wraps execute() in
    executeWithTransportRetry(): one retry on SocketException /
    EOFException / generic IOException. Request builders are
    immutable, so the second pass opens a fresh connection cleanly.
    HTTP error status codes (4xx / 5xx) and malformed responses are
    NOT retried — they go to the caller as before.

  - NostrNestsHarness now polls GET /health until it returns 200
    after the port-probe succeeds. moq-auth's Node runtime opens its
    listen socket before the request handlers are wired, so a POST
    that arrives in that window can RST. Waiting for /health proves
    the request pipeline is live, eliminating the SocketException
    that hit the first test of every test class run after
    AuthEndpoints.

Symptoms fixed:
  - NostrNestsAuthFailureInteropTest.missing_authorization_header_is_rejected_401
    -> SocketException
  - NostrNestsRoundTripInteropTest.production_speaker_broadcasts_to_production_listener_via_real_relay
    -> "Failed to reach http://127.0.0.1:8090/auth"
  - NostrNestsMultiPeerInteropTest.* (3 tests, same root cause)
This commit is contained in:
Claude
2026-04-26 19:46:38 +00:00
parent 7e67c4655f
commit 74d5e77a83
2 changed files with 85 additions and 2 deletions
@@ -154,6 +154,13 @@ class NostrNestsHarness private constructor(
try {
waitForPort("127.0.0.1", AUTH_HOST_PORT, PORT_READY_TIMEOUT_MS)
waitForPort("127.0.0.1", MOQ_HOST_PORT, PORT_READY_TIMEOUT_MS)
// moq-auth's Node runtime opens the listen socket
// before its handlers are wired, so the first POST that
// arrives in the gap can RST-on-write. Wait for /health
// to actually return 200 — that proves the request
// pipeline is live and avoids the SocketException that
// otherwise hits the first test of the second class.
waitForHealth("http://127.0.0.1:$AUTH_HOST_PORT/health", PORT_READY_TIMEOUT_MS)
} catch (t: Throwable) {
// If readiness probe fails, tear down so we don't leak the
// stack into the next test run.
@@ -300,5 +307,41 @@ class NostrNestsHarness private constructor(
lastError,
)
}
/**
* Poll [healthUrl] until it returns 200. moq-auth opens its
* listen socket before its handlers are ready; without this,
* the first POST that arrives during that window gets a TCP
* RST. Uses HttpURLConnection so we don't pull a heavy HTTP
* client into the harness.
*/
private fun waitForHealth(
healthUrl: String,
timeoutMs: Long,
) {
val deadline = System.currentTimeMillis() + timeoutMs
var lastError: Throwable? = null
val url = java.net.URI(healthUrl).toURL()
while (System.currentTimeMillis() < deadline) {
try {
val conn = url.openConnection() as java.net.HttpURLConnection
conn.connectTimeout = 1_000
conn.readTimeout = 1_000
conn.requestMethod = "GET"
try {
if (conn.responseCode == 200) return
} finally {
conn.disconnect()
}
} catch (t: Throwable) {
lastError = t
}
Thread.sleep(PORT_PROBE_INTERVAL_MS)
}
throw IllegalStateException(
"$healthUrl did not return 200 within ${timeoutMs}ms",
lastError,
)
}
}
}