Timeouts and recovery
Web, macOS/Windows desktop, iOS and Android share the JSON request policy in
@repo/app-config/network, the API client and TanStack Query hooks.
| Operation | Budget | Recovery |
|---|---|---|
| Ordinary API request | 30 seconds, including waiting for authentication and the response body | Show a persistent error with Try again for reads. |
| Authentication bootstrap | 30 seconds across adapter bootstrap and refresh on web/desktop; native storage and token HTTP calls each have a 30-second bound | Offer sign-in again; temporary refresh failures preserve stored credentials. |
| Slow foreground request | Inform after 8 seconds | Keep drafts and usable cached content visible. |
| Receipt upload | 120 seconds | Abort upload; preserve the expense form. |
| Receipt/chat processing | Pause automatic polling after 180 seconds per observed job/message, or on a failed status request | Check scan status / Refresh conversation reads the existing job without submitting another paid operation. |
| Desktop renderer startup | 30 seconds to finish loading | Native dialog offers Try again / Close; also handles load failures and renderer crashes. |
These are initial budgets, to be tuned using measured latency. Polling budgets bound local observation; they do not cancel work already accepted by the server. Leaving a screen cancels its read queries and stops background polling. Transport deadlines reject even if a platform ignores the abort signal. Late authentication completion cannot send a request after its deadline. A bootstrap result is committed only while its session revision is current.
Safe recovery
Queries and mutations have no automatic TanStack retry. Offline requests start and fail within their normal deadline instead of remaining paused indefinitely. The existing paid scan submission recovery may retry a transport failure once with the same attempt ID, within a single 30-second budget. Chat retains its existing stable chat/message IDs and server save guard.
A lost write response is an unknown outcome, not proof of failure or rollback. The persistent notice says We couldn’t confirm your changes and offers Check latest data. This refetches active reads and never invokes a mutation. Users must inspect the resulting data before submitting again. General app writes do not all have server idempotency keys, so the client must not automatically replay them. Durable reconciliation and idempotency for every ordinary create action remain follow-up work in #775; restarting the app alone cannot establish whether a timed-out write succeeded.
Receipt processing keeps its accepted job ID while polling is paused and disables another scan. Users can check that job or continue entering the expense manually. Chat keeps sending disabled while the server still reports a pending message; refreshing or ending the local conversation remains available. Neither action claims to cancel the server job or refund credits.
Error notices use plain-language categories for timeouts, network failure, expired sessions, unavailable content, validation/conflicts and server failure. They use accessible live regions and native/keyboard-operable buttons; critical errors do not disappear on a timer. Existing inline form validation remains in place. The dashboard distinguishes failed initial loading from an empty account and preserves cached groups/balances after refresh failure.
Diagnostics and validation
API failures emit the schema route template, HTTP method, duration, category, retry count (zero for each request), HTTP status and available request ID. Web and desktop send these through the existing New Relic error agent; native clients use the existing action recorder. Platform and stage come from each existing agent. No actual URL, query string, token, request/response body, or exception text is included. Intentional cancellations are excluded from monitoring.
packages/app-config/test/network.test.mjs verifies deadlines, cancellation,
late rejection and body delivery. Shared UI API tests cover a hung token wait,
POST body preservation and safe recovery. Desktop tests cover startup timeout,
retry and timer cleanup. The hermetic Playwright recovery spec runs with:
npm run screenshots:mobile --workspace=web-e2e -- request-recovery.spec.ts --workers=1
It checks loading → slow → timeout → retry → success at desktop and phone widths and saves error-state screenshots to Playwright's test results. This uses mock API responses and does not claim deployed backend or native-device acceptance. Native iOS/Android, packaged desktop and screen-reader walkthroughs remain release checks: test airplane mode, a slow connection, timed-out save, preserved draft, status reconciliation, navigation during loading and successful retry.
Browser error states: desktop · phone width.