Files
opencode-mobile/src/lib/diagnostics-classify.ts
Den df4a3618c4 fix(connection): fix 6 correctness bugs in auth/connect/diagnostics flow (#131)
1. buildRequestHeaders: UTF-8-encode Basic-auth credentials before btoa()
   so non-ASCII usernames/passwords don't throw (Hermes' btoa is Latin1-only
   and the throw was an unhandled rejection that hung the connect spinner).

2. diagnostics classify(): check root.ok (server reachable) before
   !internet.ok, so a reachable-but-failing server (e.g. wrong auth) is no
   longer misdiagnosed as "no internet" just because the public-internet
   probe also failed (captive portal, Tailscale-only network, etc).

3. sdk.ts createClient: strip trailing slashes from baseUrl once, so a
   trailing-slash URL from Advanced mode / Edit screen doesn't produce a
   double slash on every request path.

4. add.tsx / [id].tsx: wrap addConnection/updateConnection in try/catch so
   a SecureStore failure after a successful test resets the spinner and
   shows an alert instead of hanging forever. Adds
   connection.shared.alerts.saveFailedTitle/saveFailedMessage (en + zh-Hans).

5. add.tsx / [id].tsx: build the diagnostics probe's auth with buildAuth()
   instead of a hand-rolled expression, so the probe reproduces the real
   request's credentials (previously Quick Connect's password-only case
   sent no auth to the probe at all).

6. add.tsx handleQuickConnect: stop sending the shared `username` state,
   which could carry a stray value typed earlier in Advanced mode and
   silently override the "opencode" default after "Back to Quick".


Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6

Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:04:33 -07:00

89 lines
3.3 KiB
TypeScript

// Pure URL parsing + connection-failure classification, extracted from diagnostics.ts
// so this decision logic (which produces the user-facing "why did connecting fail"
// guidance) is unit-testable without react-native / expo-clipboard / expo-device.
export type Classification =
| "ok"
| "malformed-url"
| "no-internet"
| "server-unreachable"
| "health-failed"
| "tls-error"
| "timeout"
| "unknown"
export interface ProbeAttempt {
name: string
target: string
ok: boolean
status?: number
durationMs: number
error?: string
errorCause?: string
}
export interface ParsedUrl {
valid: boolean
scheme?: string
host?: string
port?: string
isHostname: boolean
}
// Hermes' built-in URL is incomplete (hostname/port often unreliable), so parse
// with a regex instead of `new URL`.
export function parseUrl(url: string): ParsedUrl {
const m = url.trim().match(/^(https?):\/\/([^/:?#]+)(?::(\d+))?/i)
if (!m) return { valid: false, isHostname: false }
const scheme = m[1].toLowerCase()
const host = m[2]
const port = m[3] || (scheme === "https" ? "443" : "80")
const isHostname = !/^\d{1,3}(\.\d{1,3}){3}$/.test(host)
return { valid: true, scheme, host, port, isHostname }
}
export function classify(
parsed: ParsedUrl,
health: ProbeAttempt,
internet: ProbeAttempt,
root: ProbeAttempt,
): { classification: Classification; summary: string } {
if (!parsed.valid) {
return { classification: "malformed-url", summary: "The server URL could not be parsed. Check for typos or extra characters." }
}
if (health.ok) {
return { classification: "ok", summary: "Health endpoint responded — connection actually works now." }
}
const txt = `${health.error ?? ""} ${health.errorCause ?? ""}`.toLowerCase()
const isTls = /ssl|tls|certificate|trust|handshake/.test(txt)
const isTimeout = /timeout|timed out/.test(txt)
if (isTls) {
return { classification: "tls-error", summary: "TLS/certificate problem. Try http:// instead of https://, or fix the server certificate." }
}
// The user's own server responded to *something* (even a 401/403/404) —
// that proves the path to the server works, so a failed public-internet
// probe (captive portal, no WAN but Tailscale LAN still up, etc.) must not
// override it and misreport a reachable server as "no internet".
if (root.ok) {
return { classification: "health-failed", summary: `Server is reachable but /global/health failed (HTTP ${health.status ?? "error"}). Likely wrong path, auth, or an old server version.` }
}
if (!internet.ok) {
return { classification: "no-internet", summary: "The device has no working internet/network at all (public check also failed). Check Wi-Fi/data and Tailscale (VPN) status." }
}
// Internet works, server does not.
if (isTimeout) {
return { classification: "timeout", summary: "Connection to the server timed out (dropped, not refused). Likely a firewall, wrong port, or Tailscale ACL blocking the device." }
}
return {
classification: "server-unreachable",
summary:
`Internet works, but the server at ${parsed.host}:${parsed.port} is unreachable. ` +
(parsed.isHostname
? "Hostname may not resolve from this device (MagicDNS off?). Try the raw Tailscale IP. "
: "") +
"Confirm the opencode server is running, the device is on the same tailnet, and the port is correct.",
}
}