Skip to content

fix(probelink): retry bad-handshake/EOF dial errors — Android adb-forward race - #200

Merged
pbertsch merged 3 commits into
mainfrom
fix/android-dial-handshake-retry
Aug 8, 2026
Merged

fix(probelink): retry bad-handshake/EOF dial errors — Android adb-forward race#200
pbertsch merged 3 commits into
mainfrom
fix/android-dial-handshake-retry

Conversation

@pbertsch

@pbertsch pbertsch commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the reproducible Android connect race where probe fails instantly with websocket: bad handshake right after creating its adb port forward, while a manual forward + connect a moment later always succeeds.

Root cause: adb accepts the host-side TCP connection as soon as the forward exists, before the device-side socket is plumbed. A WebSocket upgrade attempted in that window dies mid-handshake, surfacing as websocket: bad handshake (or EOF when nothing listens device-side) instead of connection refused. isTransientDialError didn't recognize either string, so DialWithOptions treated them as fatal protocol errors and returned on the first attempt — bypassing the retry loop and ignoring --dial-timeout entirely.

Fix:

  • websocket.ErrBadHandshake is now retried within the dial-timeout window, unless the handshake response is an HTTP 401/403 — a real token rejection still fails immediately with a clear "agent rejected token" error, so stale tokens don't spin for the full timeout.
  • EOF / unexpected EOF added to the transient-error list (also benefits the physical-iOS HTTP ping path, which shares isTransientDialError).

Test plan

  • go test ./internal/probelink/... — passes
  • New TestDialRetriesAfterBadHandshake — first upgrade attempt returns non-101, second succeeds; dial must retry
  • New TestDialRetriesAfterImmediateClose — TCP accept-then-close (adb with dead device side), then real server; dial must retry
  • New TestDialFailsFastOnAuthReject — 401 fails in <3s despite 30s dial timeout
  • New TestIsTransientDialError table test incl. EOF variants
  • go vet + go build ./... clean
  • Manual verification against a physical Android device (needs hardware)

pbertsch added 2 commits June 28, 2026 15:01
…nnect

adb forward accepts the host-side TCP connection before the device-side
socket is plumbed, so a WebSocket upgrade attempted immediately after
creating the forward fails with "websocket: bad handshake" (or EOF on a
port with no device listener). Both errors were classified as fatal
protocol errors, bypassing the retry loop entirely — the dial failed
instantly regardless of --dial-timeout, while a manual connect a moment
later succeeded.

Bad handshake and EOF are now retried within the dial-timeout window.
A genuine HTTP 401/403 token rejection from the agent is detected via
the handshake response and still fails immediately, so stale tokens
don't spin for the full timeout.

Claude-Session: https://claude.ai/code/session_01BAhxxcGrCFAv6VWpgztLrF
@pbertsch
pbertsch requested a review from a team as a code owner August 8, 2026 16:38
…shake-retry

# Conflicts:
#	CHANGELOG.md
#	internal/probelink/client.go
#	internal/probelink/client_test.go
@pbertsch
pbertsch merged commit 34de296 into main Aug 8, 2026
13 checks passed
@pbertsch
pbertsch deleted the fix/android-dial-handshake-retry branch August 8, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant