Skip to content

[Bug]: Android store app disconnects immediately after server.getConfig — RPC protocol skew with all installable server builds #4901

Description

@coreldh

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Closest match is #4671 (closed), which reported the same symptom for a nightly desktop + older Android app. This is the same failure mode, but now it happens with the public Play Store app against released stable 0.0.31, i.e. the default path a new user takes.

Area

apps/server

(The WS/RPC boundary is in the server; the mismatched side is most likely the published mobile binary. See analysis.)

Steps to reproduce

  1. macOS, T3 Code desktop 0.0.31 running. Its embedded server listens on *:3773 (all interfaces).
  2. Install the T3 Code Android app from the Play Store (build available 2026-07-29).
  3. Put the phone on the same Wi-Fi as the Mac.
  4. Mint a pairing token on the Mac:
    t3 auth pairing create --ttl 45m --base-url http://<LAN-IP>:3773
  5. In the app: Add Environment → HOST http://<LAN-IP>:3773, plus the pairing code.

Expected behavior

The environment connects and stays connected.

If the client's RPC protocol version is not supported by the server, the handshake should fail with an explicit, actionable error (e.g. "client too old, update the app") instead of closing the socket silently and retrying forever.

Actual behavior

The environment is added successfully and displays the correct label pulled from /.well-known/t3/environment — then immediately enters:

Failed to connect. Reconnecting… Reason: <label> connection failed unexpectedly

…and retries indefinitely.

What actually happens on the wire, per server traces:

Stage Result
GET /.well-known/t3/environment ✅ 200
POST /oauth/token (token exchange) ✅ 200, bearer session created
POST /api/auth/websocket-ticket ✅ 200
GET /ws upgrade + authenticateWebSocketUpgrade ✅ Success
SessionStore.markConnected ✅ Success
ws.rpc.server.getConfig Success (~40 ms)
Socket survival after that first RPC ❌ closes 75–178 ms after upgrade

Exactly one RPC is ever dispatched (server.getConfig). It succeeds. Then the socket dies. There is no server-side error in the trace — the only failures are benign CommandResolutionErrors from externalLauncher.resolveAvailableEditors probing editors that aren't installed.

Control test: a plain Node ws client from the same LAN, using a ticket issued the same way and mimicking the app's headers (Origin, User-Agent: okhttp/4.9.2, permessage-deflate), upgrades with 101 and holds the socket open indefinitely (verified 8 s, closed by me). So the server's upgrade path, deflate negotiation and auth are all healthy; the failure is specific to the app's RPC conversation.

Note for triagers: /ws spans record http.response.status_code: 204. That is not a symptom — my healthy 8-second control connection also logged 204 (it's the placeholder response for an upgraded socket). The only distinguishing signal is span duration.

Analysis: likely Effect RPC protocol skew

Per @juliusmarminge in #4671, #4643 ("Upgrade Effect and Alchemy betas", merged 2026-07-27T14:21:03Z) contained a breaking change in Effect's RPC protocol, and both sides must be updated together.

I pinned the exact publish boundary on npm:

Version Published effect
0.0.29-nightly.20260727.919 2026-07-27T13:25:55Z 4.0.0-beta.78
0.0.29-nightly.20260727.920 2026-07-27T16:10:37Z 4.0.0-beta.102

Every server a user can currently install is on the new side of that break:

  • t3@0.0.31 (latest, published 2026-07-29T20:53Z) → effect 4.0.0-beta.102
  • t3@nightly = 0.0.32-nightly.20260729.951effect 4.0.0-beta.102
  • T3 Code desktop (Alpha) 0.0.31 → bundle strings reference 4.0.0-beta.102 (185×), plus a few 4.0.0-beta.103 (8×)

Since there is no installable server on the old side and no newer one to move to, the remaining suspect is the published mobile binary being compiled before #4643 — which would make this #4671 with the roles reversed, and would explain a connection that authenticates, serves one RPC, then drops.

I want to be explicit that I could not verify the app's bundled effect version — that part is inference, not measurement. Everything in the tables above is measured. Happy to run anything specific if it helps confirm.

Impact

Blocks work completely — the mobile app cannot connect to any currently installable server build.

Version or commit

  • Server: t3@0.0.31, and T3 Code desktop (Alpha) 0.0.31 (the build actually serving, on *:3773)
  • Android app: Play Store build as of 2026-07-29
  • I did not runtime-test 0.0.32-nightly.20260729.951; I only confirmed it declares the same effect 4.0.0-beta.102.

Environment

macOS (Darwin 25.5.0, arm64) · T3 Code (Alpha) 0.0.31 embedded server on *:3773 · CLI t3 0.0.31 on Node 22.22.3 · Android client okhttp/4.9.2 · direct LAN pairing, no relay involved.

Logs or stack traces

# phone's retry loop — every HTTP step succeeds (server.trace.ndjson)
18:51:27  GET   /.well-known/t3/environment   -> 200 Success
18:51:27  POST  /oauth/token                  -> 200 Success
18:51:27  POST  /api/auth/websocket-ticket    -> 200 Success
18:51:28  GET   /.well-known/t3/environment   -> 200 Success
18:51:28  POST  /api/auth/websocket-ticket    -> 200 Success
... repeats with backoff indefinitely ...

# the /ws upgrade itself — well-formed request, auth OK, socket dies anyway
name: http.server GET
attributes:
  http.route: /ws
  http.request.header.upgrade: websocket
  http.request.header.connection: Upgrade
  http.request.header.sec-websocket-version: "13"
  http.request.header.sec-websocket-extensions: permessage-deflate
  http.request.header.origin: http://<LAN-IP>:3773
  user_agent.original: okhttp/4.9.2
  http.response.status_code: 204
durationMs: 99.647791
exit: Success

# child spans of that same trace: one RPC, succeeds, then the socket closes
EnvironmentAuth.authenticateWebSocketUpgrade   Success
SessionStore.verifyWebSocketToken              Success
SessionStore.markConnected                     Success
ws.rpc.server.getConfig                        Success   (40.4 ms)
# (the remaining 292 spans are shell.isExecutableFile from editor discovery)

# control: Node `ws` client, same LAN, same headers, same ticket flow
+18ms   UPGRADE http 101  ext="permessage-deflate"
+20ms   OPEN
+8006ms -- STILL OPEN after 8s: socket HEALTHY --

Workaround

Use the web client served by the same server from the phone's browser instead of the native app:

http://<LAN-IP>:3773/pair#token=<pairing-token>

Because that client ships with the server, its RPC protocol matches by construction. It works fine on Android Chrome and can be added to the home screen. This is a usable stopgap for anyone arriving from the launch announcement and hitting this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions