Skip to content

fix(desktop): retry the plan lookup and name the timeout that reaches users - #87

Merged
ralyodio merged 2 commits into
masterfrom
worktree-fix-connect-timeout-handling
Aug 29, 2026
Merged

fix(desktop): retry the plan lookup and name the timeout that reaches users#87
ralyodio merged 2 commits into
masterfrom
worktree-fix-connect-timeout-handling

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Traced a user-visible fetch failed (UND_ERR_CONNECT_TIMEOUT | Connect Timeout Error (attempted address: pairux.com:443, timeout: 10000ms)) back to its source. The site itself was healthy throughout — 15/15 probes returned 200 via the Railway jfk1 edge — so this is about how the desktop app handles a network path that stalls.

Two gaps turned up on the way.

The billing plan lookup bypassed the retry layer. entitlement.ts called fetch directly instead of apiFetch, then fell back to 'free' on any error. Failing closed is the right call and stays. But with no retry underneath it, one transient connect timeout silently downgrades a paying user to YouTube-only for the length of the cache, with only a console.warn to show for it. It now goes through apiFetch, so a blip gets ridden out rather than billed as a failure.

UND_ERR_CONNECT_TIMEOUT had no friendly text. That is backwards: apiFetch retries this code, so a user only ever sees it once the retries are spent — roughly 33 seconds of failed connects. It is simultaneously the code most likely to be read and the one with the least to read. Every retryable code now carries friendly text, plus UND_ERR_HEADERS_TIMEOUT, which is deliberately not retried (the request was already sent, so a POST can't be replayed safely) but surfaces through the same path.

A regression test pins the exact error shape from the original report.

Checks

The pre-commit hook was bypassed — its parallel turbo fan-out OOMs on this box. The gates were run individually instead, all green:

  • pnpm --filter @pairux/desktop typecheck
  • pnpm --filter @pairux/desktop lint
  • npx vitest run in apps/desktop — 610 tests across 58 files, all passing
  • pnpm --filter @pairux/desktop build

🤖 Generated with Claude Code

https://claude.ai/code/session_01RJFxocQAFeUPNW7oMNwe6e

… users

Two gaps found while tracing a user-visible
"fetch failed (UND_ERR_CONNECT_TIMEOUT | ...)" back to its source.

The billing plan lookup called fetch directly, bypassing apiFetch and its
retries, then failed closed to 'free'. Failing closed is right, but without a
retry a single transient connect timeout downgrades a paying user to
YouTube-only for the length of the cache. It now goes through apiFetch, so a
blip is ridden out rather than billed as a failure.

UND_ERR_CONNECT_TIMEOUT also had no entry in FRIENDLY_CODES, which is backwards:
apiFetch retries it, so a user only ever sees it once the retries are spent,
making it the code most likely to be read and the one with the least to read.
The retryable codes now all carry friendly text, as does the headers timeout,
which is not retried (the request was already sent) but surfaces the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJFxocQAFeUPNW7oMNwe6e
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

72 finding(s)

HIGH/CRITICAL: 13 | MEDIUM: 41 | LOW: 18

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH js-electron-node-integration apps/desktop/src/main/window.ts:49
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:691
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:715
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:813
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:815
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:894
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:910
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1064
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1066
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1128
HIGH sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
HIGH sh-remote-script-execution apps/turn/deploy-droplet.sh:62
MEDIUM insecure-temp-file .githooks/commit-msg:19
MEDIUM insecure-temp-file .githooks/post-commit:22
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:370
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:393
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:402
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:438
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:48
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:192
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:129
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:213
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:142
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:145
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:122
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:284
MEDIUM js-open-redirect apps/web/src/app/pricing/UpgradeButton.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:282
MEDIUM js-open-redirect apps/web/src/hooks/useDesktopHandoff.ts:24
MEDIUM redos-nested-quantifier apps/web/src/lib/deliverable.ts:11
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:124
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:393
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:396
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:405
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:608
MEDIUM manifest-install-lifecycle-script package.json:55
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:208
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:313
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:342
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:264
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:288
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:201
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:261
MEDIUM js-shell-exec-interpolation scripts/release.mjs:145
MEDIUM js-shell-exec-interpolation scripts/release.mjs:146

…and 22 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

ralyodio added a commit that referenced this pull request Aug 29, 2026
`pnpm format:check` runs over the whole repo, so these three files have been
failing the Lint job on every open PR regardless of what that PR touched --
including #87, which only changes files under apps/desktop.

Formatting only: prettier --write on the three files it named. No behaviour
change.


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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 83bfc72 into master Aug 29, 2026
14 of 15 checks passed
ralyodio added a commit that referenced this pull request Aug 29, 2026
…90)

`usePointerLock` suppresses the mousemove that Chromium pairs with a
pointermove by checking `performance.now() - lastPointerMoveAt < 16`. The test
dispatches the pair on consecutive lines and expects the second to be dropped.

That makes real elapsed time part of the assertion. On a loaded CI runner more
than 16ms can pass between those two dispatches, the mousemove is taken as a
second movement, and the pointer reads 0.7 instead of 0.6 -- a red build with
nothing wrong in the code. It failed exactly this way on PR #87, which touches
only apps/desktop, while the identical desktop copy of the test passed in the
same run.

Freezes performance.now across the paired dispatch in both copies. Confirmed by
running the same test with a clock stepping 20ms per call, which reproduces the
CI diff exactly (0.7 vs 0.6). Nothing that ships changes -- the 16ms window is
still the shipped behaviour, it just is not a race in the test any more.


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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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