Consola: arreglar build roto + token URL-encoded en WS + diagnóstico a Logcat
ConsolaService.consume no compilaba con Kotlin 2.2.0 (when no exhaustivo: el var ev no recibía smart-cast a no-nulo). Por eso el diagnóstico de WS nunca llegaba al APK instalado. Se fija con `val e = ev ?: break`. GatewayClient: el token iba crudo en el query del WS; un '+', '/' o '=' (base64/JWT) rompía el handshake (401/400) aunque funcionara por header en /rpc. Ahora va con URLEncoder.encode. Además, fallo de WS con stack completo a Logcat (tag ConsolaWS) + cuerpo de respuesta HTTP, y detalle en pantalla que nunca queda vacío (incluye la URL si no hay respuesta). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Semantic delta
No definition changed: this change is text, documentation or data.
2 files · +36 −7
diff --git a/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt b/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
index 4eba9da..8d67968 100644
--- a/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
+++ b/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
@@ -1,5 +1,6 @@
package com.rizoma.consola.data
+import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
@@ -12,6 +13,7 @@ import okhttp3.WebSocketListener
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.json.JSONObject
+import java.net.URLEncoder
import java.util.concurrent.TimeUnit
/**
@@ -73,9 +75,16 @@ class GatewayClient(@Volatile var config: GatewayConfig) {
url.startsWith("http://") -> "ws://" + url.removePrefix("http://")
else -> url
}
+ // El token también va en el query: muchos gateways no pueden leer la
+ // cabecera Authorization en el handshake WS (los WebSocket de navegador
+ // no la permiten). DEBE ir URL-encoded: un token con '+', '/' o '='
+ // (base64/JWT) crudo rompe el query y el server contesta 401/400 al
+ // upgrade aunque el mismo token funcione por header en /rpc.
if (config.token.isNotBlank()) {
- url += "?token=" + config.token
+ url += "?token=" + URLEncoder.encode(config.token, "UTF-8")
}
+ // URL sin query: para logs y para el mensaje de error sin filtrar el token.
+ val safeUrl = url.substringBefore('?')
val req = Request.Builder()
.url(url)
.apply { if (config.token.isNotBlank()) header("Authorization", "Bearer ${config.token}") }
@@ -120,21 +129,40 @@ class GatewayClient(@Volatile var config: GatewayConfig) {
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
- // Detalle diagnóstico: el handshake suele fallar ANTES del
- // primer mensaje. Sin esto, una caída de conexión llega como
- // "fallo del WebSocket" a secas (t.message null) y no se sabe
- // si es 401, conexión rechazada, proxy sin upgrade, o TLS.
+ // El cuerpo de la respuesta de error (si el server lo mandó) suele
+ // explicar el rechazo del upgrade; léelo ANTES de armar el detalle.
+ val httpBody = response?.let { runCatching { it.body?.string() }.getOrNull() }
+
+ // Stack completo a Logcat: la única forma fiable de ver la causa
+ // raíz cuando en pantalla solo cabe una línea. Captúralo con:
+ // adb logcat -s ConsolaWS
+ Log.w(TAG, "WS handshake/stream falló contra $safeUrl (HTTP ${response?.code ?: "—"})", t)
+ if (!httpBody.isNullOrBlank()) Log.w(TAG, "cuerpo de respuesta: ${httpBody.take(500)}")
+
+ // Detalle diagnóstico para la UI: el handshake suele fallar ANTES
+ // del primer mensaje. Sin esto, una caída llega como "fallo del
+ // WebSocket" a secas (t.message null, response null) y no se sabe
+ // si es 401, conexión rechazada, proxy sin upgrade, o TLS. Por eso
+ // siempre incluimos algo: clase, HTTP, cuerpo, o al menos la URL.
val detail = buildString {
append(t.javaClass.simpleName)
t.message?.let { append(": "); append(it) }
response?.let { append(" · HTTP "); append(it.code) }
+ if (!httpBody.isNullOrBlank()) { append(" · "); append(httpBody.trim().take(120)) }
+ if (t.message == null && response == null) {
+ append(" · sin respuesta al abrir "); append(safeUrl)
+ }
}
listener.onError(detail)
- listener.onClosed(t.message)
+ listener.onClosed(detail)
}
})
return PtyConnection(ws)
}
+
+ private companion object {
+ const val TAG = "ConsolaWS"
+ }
}
/** Eventos de una conexión PTY (callbacks desde el hilo de OkHttp). */
diff --git a/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt b/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt
index 693901b..6b04955 100644
--- a/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt
+++ b/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt
@@ -160,7 +160,8 @@ class ConsolaService : Service() {
var ev: TermEvent? = first
var changed = false
while (ev != null) {
- when (val e = ev) {
+ val e = ev ?: break
+ when (e) {
is TermEvent.Bytes -> { emulator.feed(e.data); changed = true }
is TermEvent.Resize -> {
emulator.resize(e.rows, e.cols)